QQuickListView: implement support for reusing items

This patch will implement delegate item recycling in
ListView. The API will be the same as used in TableView, except
that it will be off by default since the behavior of a delegate
that is reused is not compatible with legacy applications
which were not written with this in mind.

Most importantly:

- Component.onCompleted will only be called on a delegate the
first time it's created, and never when it's reused.

- Any user-declared properties in the delegate (that is, not
model roles, index, etc) will not be cleared or updated when an
item is reused. The application must do this manually upon
receiving the pooled or reused signal in the item.

[ChangeLog][ListView] ListView now has support for reusing delegate
items. This can be switched on by setting the reuseItems property of
ListView to true.

Task-number: QTBUG-80507
Change-Id: I68cc8300b050e4a1f89feebb1d31a2fd9189c793
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Richard Moe Gustavsen 2019-09-24 15:09:34 +02:00
parent 8c72e634b3
commit 1841a9e41d
13 changed files with 718 additions and 28 deletions

View File

@ -497,7 +497,7 @@ bool QQuickGridViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, qreal
// We've jumped more than a page. Estimate which items are now // We've jumped more than a page. Estimate which items are now
// visible and fill from there. // visible and fill from there.
int count = (fillFrom - (rowPos + rowSize())) / (rowSize()) * columns; int count = (fillFrom - (rowPos + rowSize())) / (rowSize()) * columns;
releaseVisibleItems(); releaseVisibleItems(reusableFlag);
modelIndex += count; modelIndex += count;
if (modelIndex >= model->count()) if (modelIndex >= model->count())
modelIndex = model->count() - 1; modelIndex = model->count() - 1;
@ -576,7 +576,7 @@ void QQuickGridViewPrivate::removeItem(FxViewItem *item)
item->releaseAfterTransition = true; item->releaseAfterTransition = true;
releasePendingTransition.append(item); releasePendingTransition.append(item);
} else { } else {
releaseItem(item); releaseItem(item, QQmlDelegateModel::NotReusable);
} }
} }

View File

@ -197,6 +197,8 @@ void QQuickItemView::setModel(const QVariant &m)
disconnect(d->model, SIGNAL(initItem(int,QObject*)), this, SLOT(initItem(int,QObject*))); disconnect(d->model, SIGNAL(initItem(int,QObject*)), this, SLOT(initItem(int,QObject*)));
disconnect(d->model, SIGNAL(createdItem(int,QObject*)), this, SLOT(createdItem(int,QObject*))); disconnect(d->model, SIGNAL(createdItem(int,QObject*)), this, SLOT(createdItem(int,QObject*)));
disconnect(d->model, SIGNAL(destroyingItem(QObject*)), this, SLOT(destroyingItem(QObject*))); disconnect(d->model, SIGNAL(destroyingItem(QObject*)), this, SLOT(destroyingItem(QObject*)));
disconnect(d->model, SIGNAL(itemPooled(int, QObject *)), this, SLOT(onItemPooled(int, QObject *)));
disconnect(d->model, SIGNAL(itemReused(int, QObject *)), this, SLOT(onItemReused(int, QObject *)));
} }
QQmlInstanceModel *oldModel = d->model; QQmlInstanceModel *oldModel = d->model;
@ -232,6 +234,8 @@ void QQuickItemView::setModel(const QVariant &m)
connect(d->model, SIGNAL(createdItem(int,QObject*)), this, SLOT(createdItem(int,QObject*))); connect(d->model, SIGNAL(createdItem(int,QObject*)), this, SLOT(createdItem(int,QObject*)));
connect(d->model, SIGNAL(initItem(int,QObject*)), this, SLOT(initItem(int,QObject*))); connect(d->model, SIGNAL(initItem(int,QObject*)), this, SLOT(initItem(int,QObject*)));
connect(d->model, SIGNAL(destroyingItem(QObject*)), this, SLOT(destroyingItem(QObject*))); connect(d->model, SIGNAL(destroyingItem(QObject*)), this, SLOT(destroyingItem(QObject*)));
connect(d->model, SIGNAL(itemPooled(int, QObject *)), this, SLOT(onItemPooled(int, QObject *)));
connect(d->model, SIGNAL(itemReused(int, QObject *)), this, SLOT(onItemReused(int, QObject *)));
if (isComponentComplete()) { if (isComponentComplete()) {
d->updateSectionCriteria(); d->updateSectionCriteria();
d->refill(); d->refill();
@ -692,6 +696,28 @@ void QQuickItemView::setHighlightMoveDuration(int duration)
} }
} }
bool QQuickItemView::reuseItems() const
{
return bool(d_func()->reusableFlag == QQmlDelegateModel::Reusable);
}
void QQuickItemView::setReuseItems(bool reuse)
{
Q_D(QQuickItemView);
if (reuseItems() == reuse)
return;
d->reusableFlag = reuse ? QQmlDelegateModel::Reusable : QQmlDelegateModel::NotReusable;
if (!reuse && d->model) {
// When we're told to not reuse items, we
// immediately, as documented, drain the pool.
d->model->drainReusableItemsPool(0);
}
emit reuseItemsChanged();
}
QQuickTransition *QQuickItemView::populateTransition() const QQuickTransition *QQuickItemView::populateTransition() const
{ {
Q_D(const QQuickItemView); Q_D(const QQuickItemView);
@ -846,7 +872,7 @@ void QQuickItemViewPrivate::positionViewAtIndex(int index, int mode)
setPosition(qMin(itemPos, maxExtent)); setPosition(qMin(itemPos, maxExtent));
// now release the reference to all the old visible items. // now release the reference to all the old visible items.
for (FxViewItem *item : oldVisible) for (FxViewItem *item : oldVisible)
releaseItem(item); releaseItem(item, reusableFlag);
item = visibleItem(idx); item = visibleItem(idx);
} }
if (item) { if (item) {
@ -1089,8 +1115,8 @@ qreal QQuickItemViewPrivate::calculatedMaxExtent() const
void QQuickItemViewPrivate::applyDelegateChange() void QQuickItemViewPrivate::applyDelegateChange()
{ {
releaseVisibleItems(); releaseVisibleItems(QQmlDelegateModel::NotReusable);
releaseItem(currentItem); releaseItem(currentItem, QQmlDelegateModel::NotReusable);
currentItem = nullptr; currentItem = nullptr;
updateSectionCriteria(); updateSectionCriteria();
refill(); refill();
@ -1192,7 +1218,7 @@ void QQuickItemView::destroyRemoved()
} else { } else {
if (hasRemoveTransition) if (hasRemoveTransition)
d->runDelayedRemoveTransition = true; d->runDelayedRemoveTransition = true;
d->releaseItem(item); d->releaseItem(item, d->reusableFlag);
it = d->visibleItems.erase(it); it = d->visibleItems.erase(it);
} }
} else { } else {
@ -1636,7 +1662,7 @@ void QQuickItemViewPrivate::updateCurrent(int modelIndex)
if (currentItem) { if (currentItem) {
if (currentItem->attached) if (currentItem->attached)
currentItem->attached->setIsCurrentItem(false); currentItem->attached->setIsCurrentItem(false);
releaseItem(currentItem); releaseItem(currentItem, reusableFlag);
currentItem = nullptr; currentItem = nullptr;
currentIndex = modelIndex; currentIndex = modelIndex;
emit q->currentIndexChanged(); emit q->currentIndexChanged();
@ -1673,7 +1699,7 @@ void QQuickItemViewPrivate::updateCurrent(int modelIndex)
if (oldCurrentItem != currentItem if (oldCurrentItem != currentItem
&& (!oldCurrentItem || !currentItem || oldCurrentItem->item != currentItem->item)) && (!oldCurrentItem || !currentItem || oldCurrentItem->item != currentItem->item))
emit q->currentItemChanged(); emit q->currentItemChanged();
releaseItem(oldCurrentItem); releaseItem(oldCurrentItem, reusableFlag);
} }
void QQuickItemViewPrivate::clear(bool onDestruction) void QQuickItemViewPrivate::clear(bool onDestruction)
@ -1683,17 +1709,17 @@ void QQuickItemViewPrivate::clear(bool onDestruction)
bufferedChanges.reset(); bufferedChanges.reset();
timeline.clear(); timeline.clear();
releaseVisibleItems(); releaseVisibleItems(QQmlInstanceModel::NotReusable);
visibleIndex = 0; visibleIndex = 0;
for (FxViewItem *item : qAsConst(releasePendingTransition)) { for (FxViewItem *item : qAsConst(releasePendingTransition)) {
item->releaseAfterTransition = false; item->releaseAfterTransition = false;
releaseItem(item); releaseItem(item, QQmlInstanceModel::NotReusable);
} }
releasePendingTransition.clear(); releasePendingTransition.clear();
auto oldCurrentItem = currentItem; auto oldCurrentItem = currentItem;
releaseItem(currentItem); releaseItem(currentItem, QQmlDelegateModel::NotReusable);
currentItem = nullptr; currentItem = nullptr;
if (oldCurrentItem) if (oldCurrentItem)
emit q->currentItemChanged(); emit q->currentItemChanged();
@ -1752,7 +1778,7 @@ void QQuickItemViewPrivate::refill(qreal from, qreal to)
if (currentChanges.hasPendingChanges() || bufferedChanges.hasPendingChanges()) { if (currentChanges.hasPendingChanges() || bufferedChanges.hasPendingChanges()) {
currentChanges.reset(); currentChanges.reset();
bufferedChanges.reset(); bufferedChanges.reset();
releaseVisibleItems(); releaseVisibleItems(reusableFlag);
} }
int prevCount = itemCount; int prevCount = itemCount;
@ -1916,7 +1942,7 @@ void QQuickItemViewPrivate::layout()
continue; continue;
} }
if (!success) { if (!success) {
releaseItem(*it); releaseItem(*it, reusableFlag);
it = releasePendingTransition.erase(it); it = releasePendingTransition.erase(it);
continue; continue;
} }
@ -2063,7 +2089,7 @@ bool QQuickItemViewPrivate::applyModelChanges(ChangeResult *totalInsertionResult
prepareRemoveTransitions(&currentChanges.removedItems); prepareRemoveTransitions(&currentChanges.removedItems);
for (QHash<QQmlChangeSet::MoveKey, FxViewItem *>::Iterator it = currentChanges.removedItems.begin(); for (QHash<QQmlChangeSet::MoveKey, FxViewItem *>::Iterator it = currentChanges.removedItems.begin();
it != currentChanges.removedItems.end(); ++it) { it != currentChanges.removedItems.end(); ++it) {
releaseItem(it.value()); releaseItem(it.value(), reusableFlag);
} }
currentChanges.removedItems.clear(); currentChanges.removedItems.clear();
@ -2072,7 +2098,7 @@ bool QQuickItemViewPrivate::applyModelChanges(ChangeResult *totalInsertionResult
if (currentItem->item && currentItem->attached) if (currentItem->item && currentItem->attached)
currentItem->attached->setIsCurrentItem(false); currentItem->attached->setIsCurrentItem(false);
auto oldCurrentItem = currentItem; auto oldCurrentItem = currentItem;
releaseItem(currentItem); releaseItem(currentItem, reusableFlag);
currentItem = nullptr; currentItem = nullptr;
if (oldCurrentItem) if (oldCurrentItem)
emit q->currentItemChanged(); emit q->currentItemChanged();
@ -2279,7 +2305,7 @@ void QQuickItemViewPrivate::viewItemTransitionFinished(QQuickItemViewTransitiona
{ {
for (int i=0; i<releasePendingTransition.count(); i++) { for (int i=0; i<releasePendingTransition.count(); i++) {
if (releasePendingTransition.at(i)->transitionableItem == item) { if (releasePendingTransition.at(i)->transitionableItem == item) {
releaseItem(releasePendingTransition.takeAt(i)); releaseItem(releasePendingTransition.takeAt(i), reusableFlag);
return; return;
} }
} }
@ -2385,7 +2411,23 @@ void QQuickItemView::destroyingItem(QObject *object)
} }
} }
bool QQuickItemViewPrivate::releaseItem(FxViewItem *item) void QQuickItemView::onItemPooled(int modelIndex, QObject *object)
{
Q_UNUSED(modelIndex);
if (auto *attached = d_func()->getAttachedObject(object))
emit attached->pooled();
}
void QQuickItemView::onItemReused(int modelIndex, QObject *object)
{
Q_UNUSED(modelIndex);
if (auto *attached = d_func()->getAttachedObject(object))
emit attached->reused();
}
bool QQuickItemViewPrivate::releaseItem(FxViewItem *item, QQmlInstanceModel::ReusableFlag reusableFlag)
{ {
Q_Q(QQuickItemView); Q_Q(QQuickItemView);
if (!item) if (!item)
@ -2396,13 +2438,15 @@ bool QQuickItemViewPrivate::releaseItem(FxViewItem *item)
QQmlInstanceModel::ReleaseFlags flags = {}; QQmlInstanceModel::ReleaseFlags flags = {};
if (model && item->item) { if (model && item->item) {
flags = model->release(item->item); flags = model->release(item->item, reusableFlag);
if (!flags) { if (!flags) {
// item was not destroyed, and we no longer reference it. // item was not destroyed, and we no longer reference it.
QQuickItemPrivate::get(item->item)->setCulled(true); QQuickItemPrivate::get(item->item)->setCulled(true);
unrequestedItems.insert(item->item, model->indexOf(item->item, q)); unrequestedItems.insert(item->item, model->indexOf(item->item, q));
} else if (flags & QQmlInstanceModel::Destroyed) { } else if (flags & QQmlInstanceModel::Destroyed) {
item->item->setParentItem(nullptr); item->item->setParentItem(nullptr);
} else if (flags & QQmlInstanceModel::Pooled) {
item->setVisible(false);
} }
} }
delete item; delete item;

View File

@ -110,6 +110,8 @@ class Q_QUICK_PRIVATE_EXPORT QQuickItemView : public QQuickFlickable
Q_PROPERTY(qreal preferredHighlightEnd READ preferredHighlightEnd WRITE setPreferredHighlightEnd NOTIFY preferredHighlightEndChanged RESET resetPreferredHighlightEnd) Q_PROPERTY(qreal preferredHighlightEnd READ preferredHighlightEnd WRITE setPreferredHighlightEnd NOTIFY preferredHighlightEndChanged RESET resetPreferredHighlightEnd)
Q_PROPERTY(int highlightMoveDuration READ highlightMoveDuration WRITE setHighlightMoveDuration NOTIFY highlightMoveDurationChanged) Q_PROPERTY(int highlightMoveDuration READ highlightMoveDuration WRITE setHighlightMoveDuration NOTIFY highlightMoveDurationChanged)
Q_PROPERTY(bool reuseItems READ reuseItems WRITE setReuseItems NOTIFY reuseItemsChanged REVISION 15)
QML_NAMED_ELEMENT(ItemView) QML_NAMED_ELEMENT(ItemView)
QML_UNCREATABLE("ItemView is an abstract base class.") QML_UNCREATABLE("ItemView is an abstract base class.")
QML_ADDED_IN_MINOR_VERSION(1) QML_ADDED_IN_MINOR_VERSION(1)
@ -226,6 +228,9 @@ public:
int highlightMoveDuration() const; int highlightMoveDuration() const;
virtual void setHighlightMoveDuration(int); virtual void setHighlightMoveDuration(int);
bool reuseItems() const;
void setReuseItems(bool reuse);
enum PositionMode { Beginning, Center, End, Visible, Contain, SnapPosition }; enum PositionMode { Beginning, Center, End, Visible, Contain, SnapPosition };
Q_ENUM(PositionMode) Q_ENUM(PositionMode)
@ -281,6 +286,8 @@ Q_SIGNALS:
void preferredHighlightEndChanged(); void preferredHighlightEndChanged();
void highlightMoveDurationChanged(); void highlightMoveDurationChanged();
Q_REVISION(15) void reuseItemsChanged();
protected: protected:
void updatePolish() override; void updatePolish() override;
void componentComplete() override; void componentComplete() override;
@ -296,6 +303,8 @@ protected Q_SLOTS:
virtual void initItem(int index, QObject *item); virtual void initItem(int index, QObject *item);
void modelUpdated(const QQmlChangeSet &changeSet, bool reset); void modelUpdated(const QQmlChangeSet &changeSet, bool reset);
void destroyingItem(QObject *item); void destroyingItem(QObject *item);
void onItemPooled(int modelIndex, QObject *object);
void onItemReused(int modelIndex, QObject *object);
void animStopped(); void animStopped();
void trackedPositionChanged(); void trackedPositionChanged();
@ -399,6 +408,9 @@ Q_SIGNALS:
void prevSectionChanged(); void prevSectionChanged();
void nextSectionChanged(); void nextSectionChanged();
void pooled();
void reused();
public: public:
QPointer<QQuickItemView> m_view; QPointer<QQuickItemView> m_view;
bool m_isCurrent : 1; bool m_isCurrent : 1;

View File

@ -174,7 +174,7 @@ public:
void mirrorChange() override; void mirrorChange() override;
FxViewItem *createItem(int modelIndex,QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested); FxViewItem *createItem(int modelIndex,QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested);
virtual bool releaseItem(FxViewItem *item); virtual bool releaseItem(FxViewItem *item, QQmlInstanceModel::ReusableFlag reusableFlag);
QQuickItem *createHighlightItem() const; QQuickItem *createHighlightItem() const;
QQuickItem *createComponentItem(QQmlComponent *component, qreal zValue, bool createDefault = false) const; QQuickItem *createComponentItem(QQmlComponent *component, qreal zValue, bool createDefault = false) const;
@ -238,15 +238,17 @@ public:
q->polish(); q->polish();
} }
void releaseVisibleItems() { void releaseVisibleItems(QQmlInstanceModel::ReusableFlag reusableFlag) {
// make a copy and clear the visibleItems first to avoid destroyed // make a copy and clear the visibleItems first to avoid destroyed
// items being accessed during the loop (QTBUG-61294) // items being accessed during the loop (QTBUG-61294)
const QList<FxViewItem *> oldVisible = visibleItems; const QList<FxViewItem *> oldVisible = visibleItems;
visibleItems.clear(); visibleItems.clear();
for (FxViewItem *item : oldVisible) for (FxViewItem *item : oldVisible)
releaseItem(item); releaseItem(item, reusableFlag);
} }
virtual QQuickItemViewAttached *getAttachedObject(const QObject *) const { return nullptr; }
QPointer<QQmlInstanceModel> model; QPointer<QQmlInstanceModel> model;
QVariant modelVariant; QVariant modelVariant;
int itemCount; int itemCount;
@ -288,6 +290,11 @@ public:
QQmlComponent *footerComponent; QQmlComponent *footerComponent;
FxViewItem *footer; FxViewItem *footer;
// Reusing delegate items cannot be on by default for backwards compatibility.
// Reusing an item will e.g mean that Component.onCompleted will only be called for an
// item when it's created and not when it's reused, which will break legacy applications.
QQmlInstanceModel::ReusableFlag reusableFlag = QQmlInstanceModel::NotReusable;
struct MovedItem { struct MovedItem {
FxViewItem *item; FxViewItem *item;
QQmlChangeSet::MoveKey moveKey; QQmlChangeSet::MoveKey moveKey;

View File

@ -92,7 +92,7 @@ public:
FxViewItem *newViewItem(int index, QQuickItem *item) override; FxViewItem *newViewItem(int index, QQuickItem *item) override;
void initializeViewItem(FxViewItem *item) override; void initializeViewItem(FxViewItem *item) override;
bool releaseItem(FxViewItem *item) override; bool releaseItem(FxViewItem *item, QQmlInstanceModel::ReusableFlag reusableFlag) override;
void repositionItemAt(FxViewItem *item, int index, qreal sizeBuffer) override; void repositionItemAt(FxViewItem *item, int index, qreal sizeBuffer) override;
void repositionPackageItemAt(QQuickItem *item, int index) override; void repositionPackageItemAt(QQuickItem *item, int index) override;
void resetFirstItemPosition(qreal pos = 0.0) override; void resetFirstItemPosition(qreal pos = 0.0) override;
@ -139,6 +139,8 @@ public:
bool flick(QQuickItemViewPrivate::AxisData &data, qreal minExtent, qreal maxExtent, qreal vSize, bool flick(QQuickItemViewPrivate::AxisData &data, qreal minExtent, qreal maxExtent, qreal vSize,
QQuickTimeLineCallback::Callback fixupCallback, qreal velocity) override; QQuickTimeLineCallback::Callback fixupCallback, qreal velocity) override;
QQuickItemViewAttached *getAttachedObject(const QObject *object) const override;
QQuickListView::Orientation orient; QQuickListView::Orientation orient;
qreal visiblePos; qreal visiblePos;
qreal averageSize; qreal averageSize;
@ -634,15 +636,15 @@ void QQuickListViewPrivate::initializeViewItem(FxViewItem *item)
} }
} }
bool QQuickListViewPrivate::releaseItem(FxViewItem *item) bool QQuickListViewPrivate::releaseItem(FxViewItem *item, QQmlInstanceModel::ReusableFlag reusableFlag)
{ {
if (!item || !model) if (!item || !model)
return QQuickItemViewPrivate::releaseItem(item); return QQuickItemViewPrivate::releaseItem(item, reusableFlag);
QPointer<QQuickItem> it = item->item; QPointer<QQuickItem> it = item->item;
QQuickListViewAttached *att = static_cast<QQuickListViewAttached*>(item->attached); QQuickListViewAttached *att = static_cast<QQuickListViewAttached*>(item->attached);
bool released = QQuickItemViewPrivate::releaseItem(item); bool released = QQuickItemViewPrivate::releaseItem(item, reusableFlag);
if (released && it && att && att->m_sectionItem) { if (released && it && att && att->m_sectionItem) {
// We hold no more references to this item // We hold no more references to this item
int i = 0; int i = 0;
@ -682,7 +684,7 @@ bool QQuickListViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, qreal
int newModelIdx = qBound(0, modelIndex + count, model->count()); int newModelIdx = qBound(0, modelIndex + count, model->count());
count = newModelIdx - modelIndex; count = newModelIdx - modelIndex;
if (count) { if (count) {
releaseVisibleItems(); releaseVisibleItems(reusableFlag);
modelIndex = newModelIdx; modelIndex = newModelIdx;
visibleIndex = modelIndex; visibleIndex = modelIndex;
visiblePos = itemEnd + count * (averageSize + spacing); visiblePos = itemEnd + count * (averageSize + spacing);
@ -737,7 +739,7 @@ void QQuickListViewPrivate::removeItem(FxViewItem *item)
releasePendingTransition.append(item); releasePendingTransition.append(item);
} else { } else {
qCDebug(lcItemViewDelegateLifecycle) << "\treleasing stationary item" << item->index << (QObject *)(item->item); qCDebug(lcItemViewDelegateLifecycle) << "\treleasing stationary item" << item->index << (QObject *)(item->item);
releaseItem(item); releaseItem(item, reusableFlag);
} }
} }
@ -1772,6 +1774,12 @@ void QQuickListViewPrivate::setSectionHelper(QQmlContext *context, QQuickItem *s
sectionItem->setProperty("section", section); sectionItem->setProperty("section", section);
} }
QQuickItemViewAttached *QQuickListViewPrivate::getAttachedObject(const QObject *object) const
{
QObject *attachedObject = qmlAttachedPropertiesObject<QQuickListView>(object);
return static_cast<QQuickItemViewAttached *>(attachedObject);
}
//---------------------------------------------------------------------------- //----------------------------------------------------------------------------
/*! /*!
@ -3186,6 +3194,13 @@ void QQuickListView::keyPressEvent(QKeyEvent *event)
void QQuickListView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) void QQuickListView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{ {
Q_D(QQuickListView); Q_D(QQuickListView);
if (d->model) {
// When the view changes size, we force the pool to
// shrink by releasing all pooled items.
d->model->drainReusableItemsPool(0);
}
if (d->isRightToLeft()) { if (d->isRightToLeft()) {
// maintain position relative to the right edge // maintain position relative to the right edge
qreal dx = newGeometry.width() - oldGeometry.width(); qreal dx = newGeometry.width() - oldGeometry.width();

View File

@ -0,0 +1,98 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
import QtQuick 2.15
Rectangle {
id: root
width: 640
height: 480
property int rows: 500
property int columns: 20
property real delegateHeight: 30
property real delegateWidth: 50
ListView {
id: list
anchors.fill: parent
anchors.margins: 10
objectName: "list"
model: reuseModel
reuseItems: true
cacheBuffer: 0
contentWidth: columns * delegateWidth
contentHeight: rows * delegateHeight
clip: true
property int delegatesCreatedCount: 0
delegate: Item {
objectName: "delegate"
width: list.contentWidth
height: delegateHeight
property int modelIndex: index
property int reusedCount: 0
property int pooledCount: 0
property string displayBinding: display
ListView.onPooled: pooledCount++
ListView.onReused: reusedCount++
Component.onCompleted: list.delegatesCreatedCount++
Text {
id: text1
text: display + " (Model index: " + modelIndex + ", Reused count: " + reusedCount + ")"
}
}
}
}

View File

@ -5,7 +5,8 @@ macx:CONFIG -= app_bundle
HEADERS += incrementalmodel.h \ HEADERS += incrementalmodel.h \
proxytestinnermodel.h \ proxytestinnermodel.h \
randomsortmodel.h randomsortmodel.h \
reusemodel.h
SOURCES += tst_qquicklistview.cpp \ SOURCES += tst_qquicklistview.cpp \
incrementalmodel.cpp \ incrementalmodel.cpp \
proxytestinnermodel.cpp \ proxytestinnermodel.cpp \

View File

@ -0,0 +1,84 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef REUSEMODEL_H
#define REUSEMODEL_H
#include <QAbstractListModel>
#include <QList>
#include <QStringList>
class ReuseModel : public QAbstractListModel
{
Q_OBJECT
public:
ReuseModel(int rowCount, QObject *parent = nullptr)
: QAbstractListModel(parent)
, m_rowCount(rowCount)
{}
int rowCount(const QModelIndex & = QModelIndex()) const override
{
return m_rowCount;
}
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid())
return QVariant();
switch (role) {
case Qt::DisplayRole:
return displayStringForRow(index.row());
default:
break;
}
return QVariant();
}
QString displayStringForRow(int row) const
{
return row % 2 == 0 ?
QStringLiteral("Even%1").arg(row) :
QStringLiteral("Odd%1").arg(row);
}
QHash<int, QByteArray> roleNames() const override
{
return {
{Qt::DisplayRole, "display"},
};
}
private:
int m_rowCount;
};
#endif

View File

@ -49,6 +49,7 @@
#include "incrementalmodel.h" #include "incrementalmodel.h"
#include "proxytestinnermodel.h" #include "proxytestinnermodel.h"
#include "randomsortmodel.h" #include "randomsortmodel.h"
#include "reusemodel.h"
#include <math.h> #include <math.h>
Q_DECLARE_METATYPE(Qt::LayoutDirection) Q_DECLARE_METATYPE(Qt::LayoutDirection)
@ -284,6 +285,9 @@ private slots:
void delegateWithRequiredProperties(); void delegateWithRequiredProperties();
void reuse_reuseIsOffByDefault();
void reuse_checkThatItemsAreReused();
private: private:
template <class T> void items(const QUrl &source); template <class T> void items(const QUrl &source);
template <class T> void changed(const QUrl &source); template <class T> void changed(const QUrl &source);
@ -9215,6 +9219,123 @@ void tst_QQuickListView::delegateWithRequiredProperties()
} }
} }
void tst_QQuickListView::reuse_reuseIsOffByDefault()
{
// Check that delegate recycling is off by default. The reason is that
// ListView needs to be backwards compatible with legacy applications. And
// when using delegate recycling, there are certain differences, like that
// a delegates Component.onCompleted will just be called the first time the
// item is created, and not when it's reused.
QScopedPointer<QQuickView> window(createView());
window->setSource(testFileUrl("listviewtest.qml"));
window->resize(640, 480);
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window.data()));
QQuickListView *listView = findItem<QQuickListView>(window->rootObject(), "list");
QVERIFY(listView != nullptr);
QVERIFY(!listView->reuseItems());
}
void tst_QQuickListView::reuse_checkThatItemsAreReused()
{
// Flick up and down one page of items. Check that this results in the
// delegate items being reused once.
// Note that this is slightly different from tableview, which will reuse the items
// twice during a similar down-then-up flick. The reason is that listview fills up
// free space in the view with items _before_ it release old items that have been
// flicked out. But changing this will break other auto tests (and perhaps legacy
// apps), so we have chosen to stick with this behavior for now.
QScopedPointer<QQuickView> window(createView());
ReuseModel model(100);
QQmlContext *ctxt = window->rootContext();
ctxt->setContextProperty("reuseModel", &model);
window->setSource(testFileUrl("reusedelegateitems.qml"));
window->resize(640, 480);
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window.data()));
QQuickListView *listView = findItem<QQuickListView>(window->rootObject(), "list");
QTRY_VERIFY(listView != nullptr);
const auto itemView_d = QQuickItemViewPrivate::get(listView);
QVERIFY(listView->reuseItems());
auto items = findItems<QQuickItem>(listView, "delegate");
const int initialItemCount = items.count();
QVERIFY(initialItemCount > 0);
// Sanity check that the size of the initial list of items match the count we tracked from QML
QCOMPARE(listView->property("delegatesCreatedCount").toInt(), initialItemCount);
// Go through all the initial items and check that they have not been reused yet
for (const auto item : qAsConst(items))
QCOMPARE(item->property("reusedCount").toInt(), 0);
// Flick one page down and count how many items we have created thus
// far. We expect this number to be twice as high as the initial count
// since we flicked one whole page.
const qreal delegateHeight = items.at(0)->height();
const qreal flickDistance = (initialItemCount * delegateHeight) + 1;
listView->setContentY(flickDistance);
QVERIFY(QQuickTest::qWaitForItemPolished(listView));
const int countAfterDownFlick = listView->property("delegatesCreatedCount").toInt();
QCOMPARE(countAfterDownFlick, initialItemCount * 2);
// Check that the reuse pool is now populated. We expect all initial items to be pooled,
// except model index 0, which was never reused or released, since it's ListView.currentItem.
const int poolSizeAfterDownFlick = itemView_d->model->poolSize();
QCOMPARE(poolSizeAfterDownFlick, initialItemCount - 1);
// Go through all items and check that all model data inside the delegate
// have values updated according to their model index. Since model roles
// like 'display' are injected into the context in a special way by the
// QML model classes, we need to catch it through a binding instead (which is
// OK, since then we can also check that bindings are updated when reused).
items = findItems<QQuickItem>(listView, "delegate");
for (const auto item : qAsConst(items)) {
const QString display = item->property("displayBinding").toString();
const int modelIndex = item->property("modelIndex").toInt();
QVERIFY(modelIndex >= initialItemCount);
QCOMPARE(display, model.displayStringForRow(modelIndex));
}
// Flick one page up. This time there shouldn't be any new items created, so
// delegatesCreatedCount should remain unchanged. But while we reuse all the items
// in the pool during the flick, we also fill it up again with all the items that
// were inside the page that was flicked out.
listView->setContentY(0);
QVERIFY(QQuickTest::qWaitForItemPolished(listView));
const int countAfterUpFlick = listView->property("delegatesCreatedCount").toInt();
const int poolSizeAfterUpFlick = itemView_d->model->poolSize();
QCOMPARE(countAfterUpFlick, countAfterDownFlick);
QCOMPARE(poolSizeAfterUpFlick, initialItemCount);
// Go through all items and check that they have been reused exactly once
// (except for ListView.currentItem, which was never released).
const auto listViewCurrentItem = listView->currentItem();
items = findItems<QQuickItem>(listView, "delegate");
for (const auto item : qAsConst(items)) {
const int reusedCount = item->property("reusedCount").toInt();
if (item == listViewCurrentItem)
QCOMPARE(reusedCount, 0);
else
QCOMPARE(reusedCount, 1);
}
// Go through all items again and check that all model data inside the delegate
// have correct values now that they have been reused.
items = findItems<QQuickItem>(listView, "delegate");
for (const auto item : qAsConst(items)) {
const QString display = item->property("displayBinding").toString();
const int modelIndex = item->property("modelIndex").toInt();
QVERIFY(modelIndex < initialItemCount);
QCOMPARE(display, model.displayStringForRow(modelIndex));
}
}
QTEST_MAIN(tst_QQuickListView) QTEST_MAIN(tst_QQuickListView)
#include "tst_qquicklistview.moc" #include "tst_qquicklistview.moc"

View File

@ -0,0 +1,29 @@
QT += quick
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Refer to the documentation for the
# deprecated API to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp
RESOURCES += qml.qrc
# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =
# Additional import path used to resolve QML modules just for Qt Quick Designer
QML_DESIGNER_IMPORT_PATH =
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

View File

@ -0,0 +1,103 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtQuick module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QAbstractTableModel>
typedef QPair<QString, bool> CellData;
class TestModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int rowCount READ rowCount WRITE setRowCount NOTIFY rowCountChanged)
public:
TestModel(QObject *parent = nullptr) : QAbstractListModel(parent) { }
int rowCount(const QModelIndex & = QModelIndex()) const override { return m_rows; }
void setRowCount(int count) {
m_rows = count;
emit rowCountChanged();
}
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid())
return QVariant();
switch (role) {
case Qt::DisplayRole:
return index.row() % 2 ? QStringLiteral("type2") : QStringLiteral("type1");
default:
break;
}
return QVariant();
}
QHash<int, QByteArray> roleNames() const override
{
return {
{Qt::DisplayRole, "delegateType"},
};
}
signals:
void rowCountChanged();
private:
int m_rows = 0;
};
#include "main.moc"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<TestModel>("TestModel", 0, 1, "TestModel");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}

View File

@ -0,0 +1,171 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtQuick module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
import QtQuick 2.15
import QtQuick.Window 2.2
import QtQuick.Controls 2.5
import Qt.labs.qmlmodels 1.0
import TestModel 0.1
Window {
id: root
visible: true
width: 640
height: 480
property int rows: 500
property int columns: 20
property real delegateHeight: 10
property real delegateWidth: 50
CheckBox {
id: reuseItemsBox
text: "Reuse items"
checked: true
}
Rectangle {
anchors.fill: parent
anchors.margins: 10
anchors.topMargin: reuseItemsBox.height + 10
color: "lightgray"
ListView {
id: listView
anchors.fill: parent
reuseItems: reuseItemsBox.checked
cacheBuffer: 0
contentWidth: columns * delegateWidth
contentHeight: rows * delegateHeight
flickableDirection: Flickable.HorizontalAndVerticalFlick
clip: true
model: TestModel {
rowCount: root.rows
}
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOn }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOn }
delegate: DelegateChooser {
role: "delegateType"
DelegateChoice {
roleValue: "type1"
Item {
width: listView.contentWidth
height: delegateHeight
property int reusedCount: 0
ListView.onReused: reusedCount++
Text {
id: text1
text: "Reused count:" + reusedCount
font.pixelSize: 9
}
Row {
id: choice1
width: listView.contentWidth
height: delegateHeight
anchors.left: text1.right
anchors.leftMargin: 5
property color color: Qt.rgba(0.6, 0.6, 0.8, 1)
Component.onCompleted: {
for (var i = 0; i < columns; ++i)
cellComponent.createObject(choice1, {column: i, color: color})
}
}
}
}
DelegateChoice {
roleValue: "type2"
Item {
width: listView.contentWidth
height: delegateHeight
property int reusedCount: 0
ListView.onReused: reusedCount++
Text {
id: text2
text: "Reused count:" + reusedCount
font.pixelSize: 9
}
Row {
id: choice2
width: listView.contentWidth
height: delegateHeight
anchors.left: text2.right
anchors.leftMargin: 5
property color color: Qt.rgba(0.3, 0.3, 0.8, 1)
Component.onCompleted: {
for (var i = 0; i < columns; ++i)
cellComponent.createObject(choice2, {column: i, color: color})
}
}
}
}
}
}
}
Component {
id: cellComponent
Rectangle {
height: delegateHeight
width: delegateWidth
property int column
Text {
text: "Lorem ipsum dolor sit amet"
font.pixelSize: 9
}
}
}
}

View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>main.qml</file>
</qresource>
</RCC>