diff --git a/src/quick/items/qquickgridview.cpp b/src/quick/items/qquickgridview.cpp index e69e9cff46..5f6c194bcf 100644 --- a/src/quick/items/qquickgridview.cpp +++ b/src/quick/items/qquickgridview.cpp @@ -497,7 +497,7 @@ bool QQuickGridViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, qreal // We've jumped more than a page. Estimate which items are now // visible and fill from there. int count = (fillFrom - (rowPos + rowSize())) / (rowSize()) * columns; - releaseVisibleItems(); + releaseVisibleItems(reusableFlag); modelIndex += count; if (modelIndex >= model->count()) modelIndex = model->count() - 1; @@ -576,7 +576,7 @@ void QQuickGridViewPrivate::removeItem(FxViewItem *item) item->releaseAfterTransition = true; releasePendingTransition.append(item); } else { - releaseItem(item); + releaseItem(item, QQmlDelegateModel::NotReusable); } } diff --git a/src/quick/items/qquickitemview.cpp b/src/quick/items/qquickitemview.cpp index 857b1a1c34..e88f60db6d 100644 --- a/src/quick/items/qquickitemview.cpp +++ b/src/quick/items/qquickitemview.cpp @@ -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(createdItem(int,QObject*)), this, SLOT(createdItem(int,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; @@ -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(initItem(int,QObject*)), this, SLOT(initItem(int,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()) { d->updateSectionCriteria(); 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 { Q_D(const QQuickItemView); @@ -846,7 +872,7 @@ void QQuickItemViewPrivate::positionViewAtIndex(int index, int mode) setPosition(qMin(itemPos, maxExtent)); // now release the reference to all the old visible items. for (FxViewItem *item : oldVisible) - releaseItem(item); + releaseItem(item, reusableFlag); item = visibleItem(idx); } if (item) { @@ -1089,8 +1115,8 @@ qreal QQuickItemViewPrivate::calculatedMaxExtent() const void QQuickItemViewPrivate::applyDelegateChange() { - releaseVisibleItems(); - releaseItem(currentItem); + releaseVisibleItems(QQmlDelegateModel::NotReusable); + releaseItem(currentItem, QQmlDelegateModel::NotReusable); currentItem = nullptr; updateSectionCriteria(); refill(); @@ -1192,7 +1218,7 @@ void QQuickItemView::destroyRemoved() } else { if (hasRemoveTransition) d->runDelayedRemoveTransition = true; - d->releaseItem(item); + d->releaseItem(item, d->reusableFlag); it = d->visibleItems.erase(it); } } else { @@ -1636,7 +1662,7 @@ void QQuickItemViewPrivate::updateCurrent(int modelIndex) if (currentItem) { if (currentItem->attached) currentItem->attached->setIsCurrentItem(false); - releaseItem(currentItem); + releaseItem(currentItem, reusableFlag); currentItem = nullptr; currentIndex = modelIndex; emit q->currentIndexChanged(); @@ -1673,7 +1699,7 @@ void QQuickItemViewPrivate::updateCurrent(int modelIndex) if (oldCurrentItem != currentItem && (!oldCurrentItem || !currentItem || oldCurrentItem->item != currentItem->item)) emit q->currentItemChanged(); - releaseItem(oldCurrentItem); + releaseItem(oldCurrentItem, reusableFlag); } void QQuickItemViewPrivate::clear(bool onDestruction) @@ -1683,17 +1709,17 @@ void QQuickItemViewPrivate::clear(bool onDestruction) bufferedChanges.reset(); timeline.clear(); - releaseVisibleItems(); + releaseVisibleItems(QQmlInstanceModel::NotReusable); visibleIndex = 0; for (FxViewItem *item : qAsConst(releasePendingTransition)) { item->releaseAfterTransition = false; - releaseItem(item); + releaseItem(item, QQmlInstanceModel::NotReusable); } releasePendingTransition.clear(); auto oldCurrentItem = currentItem; - releaseItem(currentItem); + releaseItem(currentItem, QQmlDelegateModel::NotReusable); currentItem = nullptr; if (oldCurrentItem) emit q->currentItemChanged(); @@ -1752,7 +1778,7 @@ void QQuickItemViewPrivate::refill(qreal from, qreal to) if (currentChanges.hasPendingChanges() || bufferedChanges.hasPendingChanges()) { currentChanges.reset(); bufferedChanges.reset(); - releaseVisibleItems(); + releaseVisibleItems(reusableFlag); } int prevCount = itemCount; @@ -1916,7 +1942,7 @@ void QQuickItemViewPrivate::layout() continue; } if (!success) { - releaseItem(*it); + releaseItem(*it, reusableFlag); it = releasePendingTransition.erase(it); continue; } @@ -2063,7 +2089,7 @@ bool QQuickItemViewPrivate::applyModelChanges(ChangeResult *totalInsertionResult prepareRemoveTransitions(¤tChanges.removedItems); for (QHash::Iterator it = currentChanges.removedItems.begin(); it != currentChanges.removedItems.end(); ++it) { - releaseItem(it.value()); + releaseItem(it.value(), reusableFlag); } currentChanges.removedItems.clear(); @@ -2072,7 +2098,7 @@ bool QQuickItemViewPrivate::applyModelChanges(ChangeResult *totalInsertionResult if (currentItem->item && currentItem->attached) currentItem->attached->setIsCurrentItem(false); auto oldCurrentItem = currentItem; - releaseItem(currentItem); + releaseItem(currentItem, reusableFlag); currentItem = nullptr; if (oldCurrentItem) emit q->currentItemChanged(); @@ -2279,7 +2305,7 @@ void QQuickItemViewPrivate::viewItemTransitionFinished(QQuickItemViewTransitiona { for (int i=0; itransitionableItem == item) { - releaseItem(releasePendingTransition.takeAt(i)); + releaseItem(releasePendingTransition.takeAt(i), reusableFlag); 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); if (!item) @@ -2396,13 +2438,15 @@ bool QQuickItemViewPrivate::releaseItem(FxViewItem *item) QQmlInstanceModel::ReleaseFlags flags = {}; if (model && item->item) { - flags = model->release(item->item); + flags = model->release(item->item, reusableFlag); if (!flags) { // item was not destroyed, and we no longer reference it. QQuickItemPrivate::get(item->item)->setCulled(true); 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); } } delete item; diff --git a/src/quick/items/qquickitemview_p.h b/src/quick/items/qquickitemview_p.h index 6bc00411f0..521580d292 100644 --- a/src/quick/items/qquickitemview_p.h +++ b/src/quick/items/qquickitemview_p.h @@ -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(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_UNCREATABLE("ItemView is an abstract base class.") QML_ADDED_IN_MINOR_VERSION(1) @@ -226,6 +228,9 @@ public: int highlightMoveDuration() const; virtual void setHighlightMoveDuration(int); + bool reuseItems() const; + void setReuseItems(bool reuse); + enum PositionMode { Beginning, Center, End, Visible, Contain, SnapPosition }; Q_ENUM(PositionMode) @@ -281,6 +286,8 @@ Q_SIGNALS: void preferredHighlightEndChanged(); void highlightMoveDurationChanged(); + Q_REVISION(15) void reuseItemsChanged(); + protected: void updatePolish() override; void componentComplete() override; @@ -296,6 +303,8 @@ protected Q_SLOTS: virtual void initItem(int index, QObject *item); void modelUpdated(const QQmlChangeSet &changeSet, bool reset); void destroyingItem(QObject *item); + void onItemPooled(int modelIndex, QObject *object); + void onItemReused(int modelIndex, QObject *object); void animStopped(); void trackedPositionChanged(); @@ -399,6 +408,9 @@ Q_SIGNALS: void prevSectionChanged(); void nextSectionChanged(); + void pooled(); + void reused(); + public: QPointer m_view; bool m_isCurrent : 1; diff --git a/src/quick/items/qquickitemview_p_p.h b/src/quick/items/qquickitemview_p_p.h index b31f53b2c0..2942f9ddaf 100644 --- a/src/quick/items/qquickitemview_p_p.h +++ b/src/quick/items/qquickitemview_p_p.h @@ -174,7 +174,7 @@ public: void mirrorChange() override; 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 *createComponentItem(QQmlComponent *component, qreal zValue, bool createDefault = false) const; @@ -238,15 +238,17 @@ public: q->polish(); } - void releaseVisibleItems() { + void releaseVisibleItems(QQmlInstanceModel::ReusableFlag reusableFlag) { // make a copy and clear the visibleItems first to avoid destroyed // items being accessed during the loop (QTBUG-61294) const QList oldVisible = visibleItems; visibleItems.clear(); for (FxViewItem *item : oldVisible) - releaseItem(item); + releaseItem(item, reusableFlag); } + virtual QQuickItemViewAttached *getAttachedObject(const QObject *) const { return nullptr; } + QPointer model; QVariant modelVariant; int itemCount; @@ -288,6 +290,11 @@ public: QQmlComponent *footerComponent; 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 { FxViewItem *item; QQmlChangeSet::MoveKey moveKey; diff --git a/src/quick/items/qquicklistview.cpp b/src/quick/items/qquicklistview.cpp index 778f28bdd5..9206628716 100644 --- a/src/quick/items/qquicklistview.cpp +++ b/src/quick/items/qquicklistview.cpp @@ -92,7 +92,7 @@ public: FxViewItem *newViewItem(int index, QQuickItem *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 repositionPackageItemAt(QQuickItem *item, int index) override; void resetFirstItemPosition(qreal pos = 0.0) override; @@ -139,6 +139,8 @@ public: bool flick(QQuickItemViewPrivate::AxisData &data, qreal minExtent, qreal maxExtent, qreal vSize, QQuickTimeLineCallback::Callback fixupCallback, qreal velocity) override; + QQuickItemViewAttached *getAttachedObject(const QObject *object) const override; + QQuickListView::Orientation orient; qreal visiblePos; 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) - return QQuickItemViewPrivate::releaseItem(item); + return QQuickItemViewPrivate::releaseItem(item, reusableFlag); QPointer it = item->item; QQuickListViewAttached *att = static_cast(item->attached); - bool released = QQuickItemViewPrivate::releaseItem(item); + bool released = QQuickItemViewPrivate::releaseItem(item, reusableFlag); if (released && it && att && att->m_sectionItem) { // We hold no more references to this item int i = 0; @@ -682,7 +684,7 @@ bool QQuickListViewPrivate::addVisibleItems(qreal fillFrom, qreal fillTo, qreal int newModelIdx = qBound(0, modelIndex + count, model->count()); count = newModelIdx - modelIndex; if (count) { - releaseVisibleItems(); + releaseVisibleItems(reusableFlag); modelIndex = newModelIdx; visibleIndex = modelIndex; visiblePos = itemEnd + count * (averageSize + spacing); @@ -737,7 +739,7 @@ void QQuickListViewPrivate::removeItem(FxViewItem *item) releasePendingTransition.append(item); } else { 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); } +QQuickItemViewAttached *QQuickListViewPrivate::getAttachedObject(const QObject *object) const +{ + QObject *attachedObject = qmlAttachedPropertiesObject(object); + return static_cast(attachedObject); +} + //---------------------------------------------------------------------------- /*! @@ -3186,6 +3194,13 @@ void QQuickListView::keyPressEvent(QKeyEvent *event) void QQuickListView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { 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()) { // maintain position relative to the right edge qreal dx = newGeometry.width() - oldGeometry.width(); diff --git a/tests/auto/quick/qquicklistview/data/reusedelegateitems.qml b/tests/auto/quick/qquicklistview/data/reusedelegateitems.qml new file mode 100644 index 0000000000..773fb50f81 --- /dev/null +++ b/tests/auto/quick/qquicklistview/data/reusedelegateitems.qml @@ -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 + ")" + } + } + } +} diff --git a/tests/auto/quick/qquicklistview/qquicklistview.pro b/tests/auto/quick/qquicklistview/qquicklistview.pro index b08fca2b1d..10edb06549 100644 --- a/tests/auto/quick/qquicklistview/qquicklistview.pro +++ b/tests/auto/quick/qquicklistview/qquicklistview.pro @@ -5,7 +5,8 @@ macx:CONFIG -= app_bundle HEADERS += incrementalmodel.h \ proxytestinnermodel.h \ - randomsortmodel.h + randomsortmodel.h \ + reusemodel.h SOURCES += tst_qquicklistview.cpp \ incrementalmodel.cpp \ proxytestinnermodel.cpp \ diff --git a/tests/auto/quick/qquicklistview/reusemodel.h b/tests/auto/quick/qquicklistview/reusemodel.h new file mode 100644 index 0000000000..21e6739384 --- /dev/null +++ b/tests/auto/quick/qquicklistview/reusemodel.h @@ -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 +#include +#include + +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 roleNames() const override + { + return { + {Qt::DisplayRole, "display"}, + }; + } + +private: + int m_rowCount; +}; + +#endif diff --git a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp index 02c36c76ca..646c54834a 100644 --- a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp +++ b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp @@ -49,6 +49,7 @@ #include "incrementalmodel.h" #include "proxytestinnermodel.h" #include "randomsortmodel.h" +#include "reusemodel.h" #include Q_DECLARE_METATYPE(Qt::LayoutDirection) @@ -284,6 +285,9 @@ private slots: void delegateWithRequiredProperties(); + void reuse_reuseIsOffByDefault(); + void reuse_checkThatItemsAreReused(); + private: template void items(const QUrl &source); template 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 window(createView()); + window->setSource(testFileUrl("listviewtest.qml")); + window->resize(640, 480); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listView = findItem(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 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(window->rootObject(), "list"); + QTRY_VERIFY(listView != nullptr); + const auto itemView_d = QQuickItemViewPrivate::get(listView); + + QVERIFY(listView->reuseItems()); + + auto items = findItems(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(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(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(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) #include "tst_qquicklistview.moc" diff --git a/tests/manual/listview/listview.pro b/tests/manual/listview/listview.pro new file mode 100644 index 0000000000..a517b75079 --- /dev/null +++ b/tests/manual/listview/listview.pro @@ -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 diff --git a/tests/manual/listview/main.cpp b/tests/manual/listview/main.cpp new file mode 100644 index 0000000000..8b28b91ee8 --- /dev/null +++ b/tests/manual/listview/main.cpp @@ -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 +#include +#include + +typedef QPair 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 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", 0, 1, "TestModel"); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + + return app.exec(); +} diff --git a/tests/manual/listview/main.qml b/tests/manual/listview/main.qml new file mode 100644 index 0000000000..723e10ac05 --- /dev/null +++ b/tests/manual/listview/main.qml @@ -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 + } + } + } +} diff --git a/tests/manual/listview/qml.qrc b/tests/manual/listview/qml.qrc new file mode 100644 index 0000000000..5f6483ac33 --- /dev/null +++ b/tests/manual/listview/qml.qrc @@ -0,0 +1,5 @@ + + + main.qml + +