diff options
| author | Richard Moe Gustavsen <richard.gustavsen@qt.io> | 2026-04-14 15:27:36 +0200 |
|---|---|---|
| committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2026-04-25 13:25:30 +0000 |
| commit | c304697a43c8ca9d78fcf9fa8c0e5ff81ecfa015 (patch) | |
| tree | 179a51c160a84aed9a5eeac7375bc117ae3ff986 | |
| parent | c3181b86bde85fd062e18d951b95a319c835da13 (diff) | |
TableView: avoid unnecessary pooling on row/column insertion and removal
When a ViewportOnly rebuild is triggered in TableView by a row
or column insertion or removal, delegate items that survive the
operation (i.e. are not part of the removed range) should not
be pooled and reused. Instead they should simply stay in place
and have their context properties updated to reflect any shift
in flat index, row or column. This avoids a full pool cycle,
preserves any ongoing visual state such as animations, and
suppresses unnecessary pooled/reused signals.
To achieve this, released items are now staged in a temporary
m_releasedItems cache rather than moved directly to the reuse
pool. During the rebuild, resolveModelItem() first searches the
cache for an item whose QPersistentModelIndex matches the
requested cell. If found, the item is reclaimed directly.
Once the rebuild is complete, commitReleasedItems() transfers
any remaining cached items (those whose cells genuinely
disappeared) to the reuse pool, like before.
In order to achieve this, a new QPersistentModelIndex is stored
on each delegate item. That way we know which cell it originally
belonged to, even after changes to the model shifted its
row and column position.
A new auto-test, verifyThatOnlyRemovedRowsArePooled, is included
that verifies the new expected behaviour.
The changes in this patch also uncovered that the test model used
throughout in the test file was implemented with too many assumptions,
and therefore needed some TLC.
Fixes: QTBUG-116650
Fixes: QTBUG-134741
Change-Id: I0b935320b6e7dca215df438f5981a154a8c10385
Reviewed-by: SanthoshKumar Selvaraj <santhosh.kumar.selvaraj@qt.io>
(cherry picked from commit de4b7283c978ca384f6c8bf9f27387158804b601)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
| -rw-r--r-- | src/qmlmodels/qqmldelegatemodel_p_p.h | 3 | ||||
| -rw-r--r-- | src/qmlmodels/qqmlobjectmodel_p.h | 1 | ||||
| -rw-r--r-- | src/qmlmodels/qqmltableinstancemodel.cpp | 136 | ||||
| -rw-r--r-- | src/qmlmodels/qqmltableinstancemodel_p.h | 7 | ||||
| -rw-r--r-- | src/quick/items/qquicktableview.cpp | 82 | ||||
| -rw-r--r-- | src/quick/items/qquicktableview_p_p.h | 1 | ||||
| -rw-r--r-- | src/quick/items/qquicktreeview.cpp | 40 | ||||
| -rw-r--r-- | src/quick/items/qquicktreeview_p_p.h | 4 | ||||
| -rw-r--r-- | tests/auto/quick/qquicktableview/data/checkalwaysemit.qml | 2 | ||||
| -rw-r--r-- | tests/auto/quick/qquicktableview/testmodel.h | 105 | ||||
| -rw-r--r-- | tests/auto/quick/qquicktableview/tst_qquicktableview.cpp | 135 |
11 files changed, 389 insertions, 127 deletions
diff --git a/src/qmlmodels/qqmldelegatemodel_p_p.h b/src/qmlmodels/qqmldelegatemodel_p_p.h index 105c124f05..100c860b49 100644 --- a/src/qmlmodels/qqmldelegatemodel_p_p.h +++ b/src/qmlmodels/qqmldelegatemodel_p_p.h @@ -197,6 +197,8 @@ public: int modelIndex() const { return m_index; } bool hasValidModelIndex() const { return m_index >= 0; } virtual void setModelIndex(int idx, int newRow, int newColumn, bool alwaysEmit = false); + void setPersistentModelIndex(const QPersistentModelIndex &index) { m_persistentIndex = index; } + QPersistentModelIndex persistentModelIndex() const { return m_persistentIndex; } bool usesStructuredModelData() const { return m_useStructuredModelData; } @@ -322,6 +324,7 @@ private: quint16 m_scriptRef = 0; bool m_useStructuredModelData = true; + QPersistentModelIndex m_persistentIndex; }; namespace QV4 { diff --git a/src/qmlmodels/qqmlobjectmodel_p.h b/src/qmlmodels/qqmlobjectmodel_p.h index c6d50559c8..aacf30f722 100644 --- a/src/qmlmodels/qqmlobjectmodel_p.h +++ b/src/qmlmodels/qqmlobjectmodel_p.h @@ -69,6 +69,7 @@ Q_SIGNALS: void modelUpdated(const QQmlChangeSet &changeSet, bool reset); void createdItem(int index, QObject *object); void initItem(int index, QObject *object); + void updateItemProperties(int index, QObject *object, bool init); void destroyingItem(QObject *object); Q_REVISION(2, 15) void itemPooled(int index, QObject *object); Q_REVISION(2, 15) void itemReused(int index, QObject *object); diff --git a/src/qmlmodels/qqmltableinstancemodel.cpp b/src/qmlmodels/qqmltableinstancemodel.cpp index 5b758ae291..dc73a17b78 100644 --- a/src/qmlmodels/qqmltableinstancemodel.cpp +++ b/src/qmlmodels/qqmltableinstancemodel.cpp @@ -74,6 +74,11 @@ QQmlTableInstanceModel::~QQmlTableInstanceModel() deleteAllFinishedIncubationTasks(); qDeleteAll(m_modelItems); drainReusableItemsPool(0); + + // We don't expect there to be any released items at this point. + // The view must explicitly call commitReleasedItems() after each + // rebuild of the viewport, and long before the model is destructed. + Q_ASSERT(m_releasedItems.isEmpty()); } QQmlComponent *QQmlTableInstanceModel::resolveDelegate(int index) @@ -93,50 +98,102 @@ QQmlComponent *QQmlTableInstanceModel::resolveDelegate(int index) return m_delegate; } -QQmlDelegateModelItem *QQmlTableInstanceModel::resolveModelItem(int index) +QQmlDelegateModelItem *QQmlTableInstanceModel::resolveModelItem(int flatIndex, const QModelIndex &modelIndex) { + Q_ASSERT(flatIndex >= 0 && flatIndex < m_adaptorModel.count()); + // Check if an item for the given index is already loaded and ready - QQmlDelegateModelItem *modelItem = m_modelItems.value(index, nullptr); - if (modelItem) + if (QQmlDelegateModelItem *modelItem = m_modelItems.value(flatIndex, nullptr)) return modelItem; - QQmlComponent *delegate = resolveDelegate(index); + QQmlComponent *delegate = resolveDelegate(flatIndex); if (!delegate) return nullptr; - // Check if the pool contains an item that can be reused - modelItem = m_reusableItemsPool.takeItem(delegate, index); - if (modelItem) { - reuseItem(modelItem, index); - m_modelItems.insert(index, modelItem); + // Check if the temporary release cache contains the requested item + if (modelIndex.isValid()) { + const auto it = std::find_if(m_releasedItems.cbegin(), m_releasedItems.cend(), + [&modelIndex](const QQmlDelegateModelItem *item) { + return item->persistentModelIndex() == modelIndex; + }); + + if (it != m_releasedItems.cend()) { + QQmlDelegateModelItem *modelItem = *it; + // Note: a rebuild can change which delegate a DelegateChooser assigns to a cell, so the + // released item may no longer use the right delegate, even if it has the correct index. + if (modelItem->delegate() == delegate) { + m_releasedItems.erase(it); + restoreFromReleasedItemsCache(modelItem, flatIndex); + return modelItem; + } + } + } + + // Check if the pool contains an item with the same delegate that can be reused + if (QQmlDelegateModelItem *modelItem = m_reusableItemsPool.takeItem(delegate, flatIndex)) { + m_modelItems.insert(flatIndex, modelItem); + reuseItem(modelItem, flatIndex); return modelItem; } // Create a new item from scratch - modelItem = m_adaptorModel.createItem(m_metaType.data(), index); - if (modelItem) { + if (QQmlDelegateModelItem *modelItem = m_adaptorModel.createItem(m_metaType.data(), flatIndex)) { modelItem->setDelegate(delegate); - m_modelItems.insert(index, modelItem); + m_modelItems.insert(flatIndex, modelItem); return modelItem; } - qWarning() << Q_FUNC_INFO << "failed creating a model item for index: " << index; + qWarning() << Q_FUNC_INFO << "failed creating a model item for index: " << flatIndex << modelIndex; return nullptr; } +void QQmlTableInstanceModel::commitReleasedItems() +{ + // Transfer all released items from the temporary cache to the reuse pool. From + // now on, they can only be reused based on delegate type and not model index. + for (auto *item : std::as_const(m_releasedItems)) { + item->setPersistentModelIndex(QModelIndex()); + if (m_reusableItemsPool.insertItem(item)) + emit itemPooled(item->modelIndex(), item->object()); + else + destroyModelItem(item, Deferred); + } + m_releasedItems.clear(); +} + QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode incubationMode) { Q_ASSERT(m_delegate); - Q_ASSERT(index >= 0 && index < m_adaptorModel.count()); - QQmlDelegateModelItem *modelItem = resolveModelItem(index); + QQmlDelegateModelItem *modelItem = resolveModelItem(index, QModelIndex()); if (!modelItem) return nullptr; - // The model item has already been incubated. So - // just bump the ref-count and return it. - if (modelItem->object()) + // Return the incubated object, or start an async incubation task and return nullptr for now + return incubateModelItemIfNeeded(modelItem, incubationMode); +} + +QObject *QQmlTableInstanceModel::object(const QModelIndex &modelIndex, QQmlIncubator::IncubationMode incubationMode) +{ + Q_ASSERT(m_delegate); + Q_ASSERT(m_adaptorModel.adaptsAim()); + + const int flatIndex = m_adaptorModel.indexAt(modelIndex.row(), modelIndex.column()); + QQmlDelegateModelItem *modelItem = resolveModelItem(flatIndex, modelIndex); + if (!modelItem) + return nullptr; + + // Return the incubated object, or start an async incubation task and return nullptr for now + return incubateModelItemIfNeeded(modelItem, incubationMode); +} + +QObject *QQmlTableInstanceModel::incubateModelItemIfNeeded(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode) +{ + if (modelItem->object()) { + // The model item has already been incubated. So + // just bump the ref-count and return it. return modelItem->referenceObjectWeak(); + } // The object is not ready, and needs to be incubated incubateModelItem(modelItem, incubationMode); @@ -186,8 +243,15 @@ QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object, // The item is not referenced by anyone m_modelItems.remove(modelItem->modelIndex()); - if (reusable == Reusable && m_reusableItemsPool.insertItem(modelItem)) { - emit itemPooled(modelItem->modelIndex(), modelItem->object()); + if (reusable == Reusable) { + // Stage the item in a temporary cache rather than moving it directly to the reuse pool. + // The view calls commitReleasedItems() once the rebuild is done, which transfers the cache + // to the pool. Keeping a temporary cache lets resolveModelItem() match a staged item by + // persistent model index first, during viewport builds, and hand back the exact same + // delegate item that was showing that cell. That way we can avoid the otherwise full + // reuse of an item, which includes emitting pooled signals etc. This also preserves + // ongoing visual state such as animations. + m_releasedItems.append(modelItem); return QQmlInstanceModel::Pooled; } @@ -255,6 +319,21 @@ void QQmlTableInstanceModel::drainReusableItemsPool(int maxPoolTime) }); } +void QQmlTableInstanceModel::restoreFromReleasedItemsCache(QQmlDelegateModelItem *item, int newFlatIndex) +{ + // Row/column shifts elsewhere in the table may have changed this item's + // position, so update its index before handing it back from the released + // items cache. Also notify the view so it can refresh any index-dependent + // required properties (such as 'expanded' and 'hasChildren'). + m_modelItems.insert(newFlatIndex, item); + const bool alwaysEmit = true; + const bool init = false; + const int row = m_adaptorModel.rowAt(newFlatIndex); + const int column = m_adaptorModel.columnAt(newFlatIndex); + item->setModelIndex(newFlatIndex, row, column, alwaysEmit); + emit updateItemProperties(newFlatIndex, item->object(), init); +} + void QQmlTableInstanceModel::reuseItem(QQmlDelegateModelItem *item, int newModelIndex) { // Update the context properties index, row and column on @@ -266,6 +345,8 @@ void QQmlTableInstanceModel::reuseItem(QQmlDelegateModelItem *item, int newModel const int newRow = m_adaptorModel.rowAt(newModelIndex); const int newColumn = m_adaptorModel.columnAt(newModelIndex); item->setModelIndex(newModelIndex, newRow, newColumn, alwaysEmit); + if (auto *qaim = abstractItemModel()) + item->setPersistentModelIndex(qaim->index(newRow, newColumn)); // Notify the application that all 'dynamic'/role-based context data has // changed as well (their getter function will use the updated index). @@ -275,6 +356,8 @@ void QQmlTableInstanceModel::reuseItem(QQmlDelegateModelItem *item, int newModel // Inform the view that the item is recycled. This will typically result // in the view updating its own attached delegate item properties. + const bool init = false; + emit updateItemProperties(newModelIndex, item->object(), init); emit itemReused(newModelIndex, item->object()); } @@ -349,6 +432,13 @@ void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncuba QObject *object = modelItem->object(); Q_ASSERT(object); + if (auto *qaim = abstractItemModel()) { + // Optimization: store a persistent model index so that resolveModelItem(QModelIndex) + // can match this item by index after a rebuild, even if its flat index has shifted + // due to row or column insertions/removals. + modelItem->setPersistentModelIndex(qaim->index(modelItem->modelRow(), modelItem->modelColumn())); + } + // Tag the incubated object with the model item for easy retrieval upon release etc. object->setProperty(kModelItemTag, QVariant::fromValue(modelItem)); @@ -540,7 +630,11 @@ void QQmlTableInstanceModelIncubationTask::setInitialState(QObject *object) initializeRequiredProperties( modelItemToIncubate, object, tableInstanceModel->delegateModelAccess()); modelItemToIncubate->setObject(object); - emit tableInstanceModel->initItem(modelItemToIncubate->modelIndex(), object); + + const bool init = true; + const int flatIndex = modelItemToIncubate->modelIndex(); + emit tableInstanceModel->initItem(flatIndex, object); + emit tableInstanceModel->updateItemProperties(flatIndex, object, init); if (!QQmlIncubatorPrivate::get(this)->requiredProperties()->empty()) modelItemToIncubate->destroyObjectLater(); diff --git a/src/qmlmodels/qqmltableinstancemodel_p.h b/src/qmlmodels/qqmltableinstancemodel_p.h index a0a6d17535..23b6ef1597 100644 --- a/src/qmlmodels/qqmltableinstancemodel_p.h +++ b/src/qmlmodels/qqmltableinstancemodel_p.h @@ -86,6 +86,10 @@ public: const QAbstractItemModel *abstractItemModel() const override; QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) override; + QObject *object(const QModelIndex &index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested); + QObject *incubateModelItemIfNeeded(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode); + void restoreFromReleasedItemsCache(QQmlDelegateModelItem *item, int newFlatIndex); + void commitReleasedItems(); ReleaseFlags release(QObject *object, ReusableFlag reusable = NotReusable) override; void dispose(QObject *object); void cancel(int) override; @@ -123,13 +127,14 @@ private: QHash<int, QQmlDelegateModelItem *> m_modelItems; QQmlReusableDelegateModelItemsPool m_reusableItemsPool; + QList<QQmlDelegateModelItem *> m_releasedItems; QList<QQmlIncubator *> m_finishedIncubationTasks; void incubateModelItem(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode); void incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *dmIncubationTask, QQmlIncubator::Status status); void deleteIncubationTaskLater(QQmlIncubator *incubationTask); void deleteAllFinishedIncubationTasks(); - QQmlDelegateModelItem *resolveModelItem(int index); + QQmlDelegateModelItem *resolveModelItem(int flatIndex, const QModelIndex &modelIndex); void destroyModelItem(QQmlDelegateModelItem *modelItem, DestructionMode mode); void dataChangedCallback(const QModelIndex &begin, const QModelIndex &end, const QList<int> &roles); diff --git a/src/quick/items/qquicktableview.cpp b/src/quick/items/qquicktableview.cpp index 1f7e1bde90..106a60ebb7 100644 --- a/src/quick/items/qquicktableview.cpp +++ b/src/quick/items/qquicktableview.cpp @@ -2826,11 +2826,20 @@ FxTableItem *QQuickTableViewPrivate::createFxTableItem(const QPoint &cell, QQmlI Q_Q(QQuickTableView); bool ownItem = false; + QObject* object = nullptr; + const QAbstractItemModel *aim = model->abstractItemModel(); + const int modelRow = isTransposed ? logicalColumnIndex(cell.y()) : logicalRowIndex(cell.y()); + const int modelColumn = isTransposed ? logicalRowIndex(cell.x()) : logicalColumnIndex(cell.x()); + const int modelIndex = modelIndexAtCell(QPoint(modelColumn, modelRow)); + + if (tableModel && aim) { + // Prefer loading via QModelIndex so that QQmlTableInstanceModel can also + // match recently released items by model index, not just by delegate type. + object = tableModel->object(aim->index(modelRow, modelColumn), incubationMode); + } else { + object = model->object(modelIndex, incubationMode); + } - int modelIndex = modelIndexAtCell(isTransposed ? QPoint(logicalRowIndex(cell.x()), logicalColumnIndex(cell.y())) : - QPoint(logicalColumnIndex(cell.x()), logicalRowIndex(cell.y()))); - - QObject* object = model->object(modelIndex, incubationMode); if (!object) { if (model->incubationStatus(modelIndex) == QQmlIncubator::Loading) { // Item is incubating. Return nullptr for now, and let the table call this @@ -2940,6 +2949,8 @@ void QQuickTableViewPrivate::unloadItem(const QPoint &cell) const int modelIndex = modelIndexAtCell(cell); Q_TABLEVIEW_ASSERT(loadedItems.contains(modelIndex), modelIndex << cell); releaseItem(loadedItems.take(modelIndex), reusableFlag); + if (tableModel) + tableModel->commitReleasedItems(); } bool QQuickTableViewPrivate::canLoadTableEdge(Qt::Edge tableEdge, const QRectF fillRect) const @@ -3681,6 +3692,10 @@ void QQuickTableViewPrivate::processRebuildTable() updateEditItem(); updateCurrentRowAndColumn(); + // Move released items that was not reused during the rebuild to the reuse pool + if (tableModel) + tableModel->commitReleasedItems(); + emit q->layoutChanged(); qCDebug(lcTableViewDelegateLifecycle()) << "current table:" << tableLayoutToString(); @@ -4457,9 +4472,25 @@ void QQuickTableViewPrivate::itemCreatedCallback(int modelIndex, QObject*) updatePolish(); } -void QQuickTableViewPrivate::initItemCallback(int modelIndex, QObject *object) +void QQuickTableViewPrivate::updateItemProperties(int flatIndex, QObject *object, bool init) +{ + Q_Q(QQuickTableView); + const QPoint cell = cellAtModelIndex(flatIndex); + const QPoint visualCell = QPoint(visualColumnIndex(cell.x()), visualRowIndex(cell.y())); + const bool current = currentInSelectionModel(visualCell); + const bool selected = selectedInSelectionModel(visualCell); + + setRequiredProperty(kRequiredProperty_tableView, QVariant::fromValue(q), flatIndex, object, init); + setRequiredProperty(kRequiredProperty_current, QVariant::fromValue(current), flatIndex, object, init); + setRequiredProperty(kRequiredProperty_selected, QVariant::fromValue(selected), flatIndex, object, init); + setRequiredProperty(kRequiredProperty_editing, QVariant::fromValue(false), flatIndex, object, init); + setRequiredProperty(kRequiredProperty_containsDrag, QVariant::fromValue(false), flatIndex, object, init); +} + +void QQuickTableViewPrivate::initItemCallback(int flatIndex, QObject *object) { Q_Q(QQuickTableView); + Q_UNUSED(flatIndex); auto item = qobject_cast<QQuickItem*>(object); if (!item) @@ -4470,17 +4501,6 @@ void QQuickTableViewPrivate::initItemCallback(int modelIndex, QObject *object) if (auto attached = getAttachedObject(item)) attached->setView(q); - - const QPoint cell = cellAtModelIndex(modelIndex); - const QPoint visualCell = QPoint(visualColumnIndex(cell.x()), visualRowIndex(cell.y())); - const bool current = currentInSelectionModel(visualCell); - const bool selected = selectedInSelectionModel(visualCell); - - setRequiredProperty(kRequiredProperty_tableView, QVariant::fromValue(q), modelIndex, item, true); - setRequiredProperty(kRequiredProperty_current, QVariant::fromValue(current), modelIndex, object, true); - setRequiredProperty(kRequiredProperty_selected, QVariant::fromValue(selected), modelIndex, object, true); - setRequiredProperty(kRequiredProperty_editing, QVariant::fromValue(false), modelIndex, item, true); - setRequiredProperty(kRequiredProperty_containsDrag, QVariant::fromValue(false), modelIndex, item, true); } void QQuickTableViewPrivate::itemPooledCallback(int modelIndex, QObject *object) @@ -4491,20 +4511,9 @@ void QQuickTableViewPrivate::itemPooledCallback(int modelIndex, QObject *object) emit attached->pooled(); } -void QQuickTableViewPrivate::itemReusedCallback(int modelIndex, QObject *object) +void QQuickTableViewPrivate::itemReusedCallback(int flatIndex, QObject *object) { - Q_Q(QQuickTableView); - - const QPoint cell = cellAtModelIndex(modelIndex); - const QPoint visualCell = QPoint(visualColumnIndex(cell.x()), visualRowIndex(cell.y())); - const bool current = currentInSelectionModel(visualCell); - const bool selected = selectedInSelectionModel(visualCell); - - setRequiredProperty(kRequiredProperty_tableView, QVariant::fromValue(q), modelIndex, object, false); - setRequiredProperty(kRequiredProperty_current, QVariant::fromValue(current), modelIndex, object, false); - setRequiredProperty(kRequiredProperty_selected, QVariant::fromValue(selected), modelIndex, object, false); - // Note: the edit item will never be reused, so no reason to set kRequiredProperty_editing - setRequiredProperty(kRequiredProperty_containsDrag, QVariant::fromValue(false), modelIndex, object, false); + Q_UNUSED(flatIndex); if (auto item = qobject_cast<QQuickItem*>(object)) QQuickItemPrivate::get(item)->setCulled(false); @@ -4744,8 +4753,9 @@ void QQuickTableViewPrivate::connectToModel() QObjectPrivate::connect(model, &QQmlInstanceModel::createdItem, this, &QQuickTableViewPrivate::itemCreatedCallback); QObjectPrivate::connect(model, &QQmlInstanceModel::initItem, this, &QQuickTableViewPrivate::initItemCallback); - QObjectPrivate::connect(model, &QQmlTableInstanceModel::itemPooled, this, &QQuickTableViewPrivate::itemPooledCallback); - QObjectPrivate::connect(model, &QQmlTableInstanceModel::itemReused, this, &QQuickTableViewPrivate::itemReusedCallback); + QObjectPrivate::connect(model, &QQmlInstanceModel::itemPooled, this, &QQuickTableViewPrivate::itemPooledCallback); + QObjectPrivate::connect(model, &QQmlInstanceModel::itemReused, this, &QQuickTableViewPrivate::itemReusedCallback); + QObjectPrivate::connect(model, &QQmlInstanceModel::updateItemProperties, this, &QQuickTableViewPrivate::updateItemProperties); // Connect atYEndChanged to a function that fetches data if more is available QObjectPrivate::connect(q, &QQuickTableView::atYEndChanged, this, &QQuickTableViewPrivate::fetchMoreData); @@ -4782,8 +4792,9 @@ void QQuickTableViewPrivate::disconnectFromModel() QObjectPrivate::disconnect(model, &QQmlInstanceModel::createdItem, this, &QQuickTableViewPrivate::itemCreatedCallback); QObjectPrivate::disconnect(model, &QQmlInstanceModel::initItem, this, &QQuickTableViewPrivate::initItemCallback); - QObjectPrivate::disconnect(model, &QQmlTableInstanceModel::itemPooled, this, &QQuickTableViewPrivate::itemPooledCallback); - QObjectPrivate::disconnect(model, &QQmlTableInstanceModel::itemReused, this, &QQuickTableViewPrivate::itemReusedCallback); + QObjectPrivate::disconnect(model, &QQmlInstanceModel::itemPooled, this, &QQuickTableViewPrivate::itemPooledCallback); + QObjectPrivate::disconnect(model, &QQmlInstanceModel::itemReused, this, &QQuickTableViewPrivate::itemReusedCallback); + QObjectPrivate::disconnect(model, &QQmlInstanceModel::updateItemProperties, this, &QQuickTableViewPrivate::updateItemProperties); QObjectPrivate::disconnect(q, &QQuickTableView::atYEndChanged, this, &QQuickTableViewPrivate::fetchMoreData); @@ -6843,7 +6854,7 @@ void QQuickTableView::edit(const QModelIndex &index) d->editModel->useImportVersion(d->resolveImportVersion()); QObject::connect(d->editModel, &QQmlInstanceModel::initItem, this, [this, d] (int serializedModelIndex, QObject *object) { - // initItemCallback will call setRequiredProperty for each required property in the + // updateItemProperties() will call setRequiredProperty for each required property in the // delegate, both for this class, but also also for any subclasses. setRequiredProperty // is currently dependent of the QQmlTableInstanceModel that was used to create the object // in order to initialize required properties, so we need to set the editItem variable @@ -6854,7 +6865,8 @@ void QQuickTableView::edit(const QModelIndex &index) if (!d->editItem) return; // Initialize required properties - d->initItemCallback(serializedModelIndex, object); + const bool init = true; + d->updateItemProperties(serializedModelIndex, object, init); const auto cellItem = itemAtCell(cellAtIndex(d->editIndex)); Q_ASSERT(cellItem); d->editItem->setParentItem(cellItem); diff --git a/src/quick/items/qquicktableview_p_p.h b/src/quick/items/qquicktableview_p_p.h index c7e3cc9119..4c7b135722 100644 --- a/src/quick/items/qquicktableview_p_p.h +++ b/src/quick/items/qquicktableview_p_p.h @@ -609,6 +609,7 @@ public: virtual void itemPooledCallback(int modelIndex, QObject *object); virtual void itemReusedCallback(int modelIndex, QObject *object); virtual void modelUpdated(const QQmlChangeSet &changeSet, bool reset); + virtual void updateItemProperties(int flatIndex, QObject *object, bool init); virtual void syncWithPendingChanges(); virtual void syncDelegate(); diff --git a/src/quick/items/qquicktreeview.cpp b/src/quick/items/qquicktreeview.cpp index d53ffc1353..91e212de74 100644 --- a/src/quick/items/qquicktreeview.cpp +++ b/src/quick/items/qquicktreeview.cpp @@ -327,18 +327,6 @@ void QQuickTreeViewPrivate::setModelImpl(const QVariant &newModel) emit q->modelChanged(); } -void QQuickTreeViewPrivate::initItemCallback(int serializedModelIndex, QObject *object) -{ - updateRequiredProperties(serializedModelIndex, object, true); - QQuickTableViewPrivate::initItemCallback(serializedModelIndex, object); -} - -void QQuickTreeViewPrivate::itemReusedCallback(int serializedModelIndex, QObject *object) -{ - updateRequiredProperties(serializedModelIndex, object, false); - QQuickTableViewPrivate::itemReusedCallback(serializedModelIndex, object); -} - void QQuickTreeViewPrivate::dataChangedCallback( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) { @@ -352,24 +340,28 @@ void QQuickTreeViewPrivate::dataChangedCallback( if (!item) continue; - const int serializedModelIndex = modelIndexAtCell(QPoint(column, row)); - updateRequiredProperties(serializedModelIndex, item, false); + const int flatIndex = column * m_treeModelToTableModel.rowCount() + row; + updateItemProperties(flatIndex, item, false); } } } -void QQuickTreeViewPrivate::updateRequiredProperties(int serializedModelIndex, QObject *object, bool init) +void QQuickTreeViewPrivate::updateItemProperties(int flatIndex, QObject *object, bool init) { Q_Q(QQuickTreeView); - const QPoint cell = cellAtModelIndex(serializedModelIndex); - const int row = cell.y(); - const int column = cell.x(); - - setRequiredProperty("treeView", QVariant::fromValue(q), serializedModelIndex, object, init); - setRequiredProperty("isTreeNode", column == kTreeColumn, serializedModelIndex, object, init); - setRequiredProperty("hasChildren", m_treeModelToTableModel.hasChildren(row), serializedModelIndex, object, init); - setRequiredProperty("expanded", q->isExpanded(row), serializedModelIndex, object, init); - setRequiredProperty("depth", m_treeModelToTableModel.depthAtRow(row), serializedModelIndex, object, init); + QQuickTableViewPrivate::updateItemProperties(flatIndex, object, init); + + // Use the model's row count rather than cellAtModelIndex(), since a pending rebuild + // can leave the table size in TableView stale relative to m_treeModelToTableModel. + const int availableRows = m_treeModelToTableModel.rowCount(); + const int column = flatIndex / availableRows; + const int row = flatIndex % availableRows; + + setRequiredProperty("treeView", QVariant::fromValue(q), flatIndex, object, init); + setRequiredProperty("isTreeNode", column == kTreeColumn, flatIndex, object, init); + setRequiredProperty("hasChildren", m_treeModelToTableModel.hasChildren(row), flatIndex, object, init); + setRequiredProperty("expanded", q->isExpanded(row), flatIndex, object, init); + setRequiredProperty("depth", m_treeModelToTableModel.depthAtRow(row), flatIndex, object, init); } void QQuickTreeViewPrivate::updateSelection(const QRect &oldSelection, const QRect &newSelection) diff --git a/src/quick/items/qquicktreeview_p_p.h b/src/quick/items/qquicktreeview_p_p.h index 88d563adb1..f7ecf0ea0a 100644 --- a/src/quick/items/qquicktreeview_p_p.h +++ b/src/quick/items/qquicktreeview_p_p.h @@ -36,13 +36,11 @@ public: QVariant modelImpl() const override; void setModelImpl(const QVariant &newModel) override; - void initItemCallback(int serializedModelIndex, QObject *object) override; - void itemReusedCallback(int serializedModelIndex, QObject *object) override; void dataChangedCallback(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles); - void updateRequiredProperties(int serializedModelIndex, QObject *object, bool init); + void updateItemProperties(int flatIndex, QObject *object, bool init) override; void updateSelection(const QRect &oldSelection, const QRect &newSelection) override; public: diff --git a/tests/auto/quick/qquicktableview/data/checkalwaysemit.qml b/tests/auto/quick/qquicktableview/data/checkalwaysemit.qml index 463e21342c..232d98fa69 100644 --- a/tests/auto/quick/qquicktableview/data/checkalwaysemit.qml +++ b/tests/auto/quick/qquicktableview/data/checkalwaysemit.qml @@ -34,7 +34,7 @@ Item { color: "lightgray" border.width: 1 - property string modelDataFromIndex: tableView.model.dataFromSerializedIndex(index) + property string modelDataFromIndex: tableView.model.dataFromFlatIndex(index) property string modelDataBinding: modelData Text { diff --git a/tests/auto/quick/qquicktableview/testmodel.h b/tests/auto/quick/qquicktableview/testmodel.h index 21ed7e03a5..26179ee4cc 100644 --- a/tests/auto/quick/qquicktableview/testmodel.h +++ b/tests/auto/quick/qquicktableview/testmodel.h @@ -46,9 +46,9 @@ public: ret = 42; break; case Qt::DisplayRole: { - int serializedIndex = index.row() + (index.column() * m_columns); - if (modelData.contains(serializedIndex)) - ret = modelData.value(serializedIndex); + const QPoint cell(index.column(), index.row()); + if (modelData.contains(cell)) + ret = modelData.value(cell); else ret = QStringLiteral("%1").arg(index.row()); } break; @@ -59,11 +59,11 @@ public: return ret; } - Q_INVOKABLE QVariant dataFromSerializedIndex(int index) const + Q_INVOKABLE QVariant dataFromFlatIndex(int flatIndex) const { - if (modelData.contains(index)) - return modelData.value(index); - return QString(); + const int row = flatIndex % m_rows; + const int column = flatIndex / m_rows; + return modelData.value(QPoint(column, row)); } QHash<int, QByteArray> roleNames() const override @@ -86,8 +86,7 @@ public: for (int r = 0; r < span.height(); ++r) { const int changedRow = cell.y() + r; const int changedColumn = cell.x() + c; - const int serializedIndex = changedRow + (changedColumn * m_rows); - modelData.insert(serializedIndex, string); + modelData.insert(QPoint(changedColumn, changedRow), string); } } @@ -103,6 +102,7 @@ public: beginInsertRows(parent, row, row + count - 1); m_rows += count; + shiftModelDataRows(row, count); endInsertRows(); return true; } @@ -114,12 +114,7 @@ public: beginRemoveRows(parent, row, row + count - 1); m_rows -= count; - for (int c = 0; c < m_columns; ++c) { - for (int r = 0; r < count; ++r) { - const int serializedIndex = (row + r) + (c * m_rows); - modelData.remove(serializedIndex); - } - } + shiftModelDataRows(row + count, -count); endRemoveRows(); return true; } @@ -131,6 +126,7 @@ public: beginInsertColumns(parent, column, column + count - 1); m_columns += count; + shiftModelDataColumns(column, count); endInsertColumns(); return true; } @@ -142,6 +138,7 @@ public: beginRemoveColumns(parent, column, column + count - 1); m_columns -= count; + shiftModelDataColumns(column + count, -count); endRemoveColumns(); return true; } @@ -155,11 +152,38 @@ public: void swapRows(int row1, int row2) { layoutAboutToBeChanged(); - Q_ASSERT(modelData.contains(row1)); - Q_ASSERT(modelData.contains(row2)); - const QString tmp = modelData[row1]; - modelData[row1] = modelData[row2]; - modelData[row2] = tmp; + for (int c = 0; c < m_columns; ++c) { + const QPoint cell1(c, row1); + const QPoint cell2(c, row2); + const QString cell1Data = modelData.value(cell1); + + // Do the data swap, but since modelData is not supposed to contain + // cells with empty strings, we need to check for this. + if (modelData.contains(cell2)) + modelData[cell1] = modelData[cell2]; + else + modelData.remove(cell1); + + if (!cell1Data.isEmpty()) + modelData[cell2] = cell1Data; + else + modelData.remove(cell2); + } + + // Since this is a row reorder and not just a data update, we must + // remap any persistent model indexes pointing into the swapped rows. + // This is required by the layoutAboutToBeChanged/layoutChanged contract. + QModelIndexList from, to; + for (const QModelIndex &idx : persistentIndexList()) { + if (idx.row() == row1) { + from << idx; + to << createIndex(row2, idx.column()); + } else if (idx.row() == row2) { + from << idx; + to << createIndex(row1, idx.column()); + } + } + changePersistentIndexList(from, to); layoutChanged(); } @@ -203,11 +227,50 @@ signals: void columnCountChanged(); private: + + void shiftModelDataRows(int firstRow, int delta) + { + // Shift all modelData entries whose row >= firstRow by delta + // (positive delta = insert, negative delta = remove). + QHash<QPoint, QString> updated; + for (auto it = modelData.begin(); it != modelData.end(); ++it) { + const int row = it.key().y(); + if (delta < 0) { + const int removedCount = -delta; + if (row < firstRow && row >= firstRow - removedCount) + continue; // row was removed + } + const int newRow = row < firstRow ? row : row + delta; + updated.insert(QPoint(it.key().x(), newRow), it.value()); + } + modelData = std::move(updated); + } + + void shiftModelDataColumns(int firstColumn, int delta) + { + // Shift all modelData entries whose column >= firstColumn by delta. + // (positive delta = insert, negative delta = remove). + QHash<QPoint, QString> updated; + for (auto it = modelData.begin(); it != modelData.end(); ++it) { + const int column = it.key().x(); + if (delta < 0) { + const int removedCount = -delta; + if (column < firstColumn && column >= firstColumn - removedCount) + continue; // column was removed + } + const int newColumn = column < firstColumn ? column : column + delta; + updated.insert(QPoint(newColumn, it.key().y()), it.value()); + } + modelData = std::move(updated); + } + +private: + int m_rows = 0; int m_columns = 0; bool m_dataCanBeFetched = false; bool m_useCustomRoleNames = false; - QHash<int, QString> modelData; + QHash<QPoint, QString> modelData; Qt::ItemFlags m_flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; }; diff --git a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp index 58bf9fbc31..226042355d 100644 --- a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp +++ b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp @@ -161,6 +161,7 @@ private slots: void checkIfDelegatesAreReused_data(); void checkIfDelegatesAreReused(); void checkIfDelegatesAreReusedAsymmetricTableSize(); + void verifyThatOnlyRemovedRowsArePooled(); void checkContextProperties_data(); void checkContextProperties(); void checkContextPropertiesQQmlListProperyModel_data(); @@ -2653,37 +2654,53 @@ void tst_QQuickTableView::checkRowAndColumnChangedButNotIndex() void tst_QQuickTableView::checkThatWeAlwaysEmitChangedUponItemReused() { - // Check that we always emit changes to index when we reuse an item, even - // if it doesn't change. This is needed since the model can have changed - // row or column count while the item was in the pool, which means that - // any data referred to by the index property inside the delegate - // will change too. So we need to refresh any bindings to index. - // QTBUG-79209 + // Verify that index, row and column are always re-emitted when an item is + // reused, even if the flat index value hasn't changed. This matters because + // the model may reset with swapped row and column counts while the item sits + // in the pool: the same flat index then maps to a different (row, column) pair, + // so all bindings that depend on those properties must be refreshed. QTBUG-79209 LOAD_TABLEVIEW("checkalwaysemit.qml"); - TestModel model(1, 1); + TestModel model(1, 2); tableView->setModel(QVariant::fromValue(&model)); - model.setModelData(QPoint(0, 0), QSize(1, 1), "old value"); + + const QPoint modifiedCell{1, 0}; + const QString text = "Lorem Ipsum"_L1; + + model.setModelData(modifiedCell, QSize(1, 1), text); WAIT_UNTIL_POLISHED; - const auto reuseItem = tableViewPrivate->loadedTableItem(QPoint(0, 0))->item; - const auto context = qmlContext(reuseItem.data()); + { + // Check that the context properties of the modified cell are as expected + const auto item = tableViewPrivate->loadedTableItem(modifiedCell)->item; + const auto context = qmlContext(item.data()); + const int flatIndex = modifiedCell.x() * model.rowCount() + modifiedCell.y(); + QCOMPARE(flatIndex, 1); + QCOMPARE(context->contextProperty("index").toInt(), flatIndex); + QCOMPARE(context->contextProperty("row").toInt(), modifiedCell.y()); + QCOMPARE(context->contextProperty("column").toInt(), modifiedCell.x()); + QCOMPARE(context->contextProperty("modelDataFromIndex").toString(), text); + } - // Remove the cell/row that has "old value" as model data, and - // add a new one right after. The new cell will have the same - // index, but with no model data assigned. - // This change will not be detected by items in the pool. But since - // we emit indexChanged when the item is reused, it will be updated then. - model.removeRow(0); - model.insertRow(0); + // Add a new row at the bottom, below the modified cell. This shouldn't affect + // its row and column position, nor the model data fetched from the + // modelDataFromIndex() binding. But since the new row changes row and column + // count, its flat index changes. + model.insertRow(1); WAIT_UNTIL_POLISHED; - QCOMPARE(context->contextProperty("index").toInt(), 0); - QCOMPARE(context->contextProperty("row").toInt(), 0); - QCOMPARE(context->contextProperty("column").toInt(), 0); - QCOMPARE(context->contextProperty("modelDataFromIndex").toString(), ""); + { + const auto item = tableViewPrivate->loadedTableItem(modifiedCell)->item; + const auto context = qmlContext(item.data()); + const int flatIndex = modifiedCell.x() * model.rowCount() + modifiedCell.y(); + QCOMPARE(flatIndex, 2); + QCOMPARE(context->contextProperty("index").toInt(), flatIndex); + QCOMPARE(context->contextProperty("row").toInt(), modifiedCell.y()); + QCOMPARE(context->contextProperty("column").toInt(), modifiedCell.x()); + QCOMPARE(context->contextProperty("modelDataFromIndex").toString(), text); + } } void tst_QQuickTableView::checkChangingModelFromDelegate() @@ -8774,6 +8791,82 @@ void tst_QQuickTableView::delegateChooserDataChange() tableView->setModel({ }); } +void tst_QQuickTableView::verifyThatOnlyRemovedRowsArePooled() +{ + // Check that we don't put delegate items above or below a removed or inserted row + // into the reuse pool. Instead those items should be unaffected by the model changes + // (other than changes to the flat index, row and column). + LOAD_TABLEVIEW("plaintableview.qml"); + + TestModel model(3, 2); + tableView->setModel(QVariant::fromValue(&model)); + + WAIT_UNTIL_POLISHED; + + QList<QPointer<QQuickItem>> initialItems; + QList<QObject *> pooledObjects; + QList<QObject *> reusedObjects; + + // Create a few helper functions that we can use further down in the test. + // getItems() returns a list of loaded delegate items that corresponds to + // the cells given as argument. + auto getItems = [&](const QList<QPoint> &cells) { + QList<QPointer<QQuickItem>> result; + for (const auto &cell : cells) + result.append(tableViewPrivate->loadedTableItem(cell)->item); + return result; + }; + + // Create a function that tracks which objects get pooled or reused, so we + // can verify that none of the unrelated cells are affected by the model changes. + auto verifyInitialItemsNotPooled = [&]() { + for (const auto &item : initialItems) { + QVERIFY(!pooledObjects.contains(item.data())); + QVERIFY(!reusedObjects.contains(item.data())); + } + }; + connect(tableViewPrivate->tableModel, &QQmlInstanceModel::itemPooled, + this, [&](int, QObject *obj) { pooledObjects.append(obj); }); + connect(tableViewPrivate->tableModel, &QQmlInstanceModel::itemReused, + this, [&](int, QObject *obj) { reusedObjects.append(obj); }); + + // Create a function that verifies that index, row and column context properties on + // each initial item reflect the expected values. The row that is below the removed + // row will have those updated. The serialized index uses (col * numRows) + row. + using Props = std::tuple<int, int, int>; // {index, row, column} + auto verifyContextProperties = [&](const QList<Props> &expected) { + for (int i = 0; i < initialItems.size(); ++i) { + const auto [expIndex, expRow, expColumn] = expected[i]; + const auto ctx = qmlContext(initialItems[i].data()); + QCOMPARE(ctx->contextProperty("index").toInt(), expIndex); + QCOMPARE(ctx->contextProperty("row").toInt(), expRow); + QCOMPARE(ctx->contextProperty("column").toInt(), expColumn); + } + }; + + // Store the items in the rows above and below the row that is about + // to be removed. Those items should never be deleted or reused. + initialItems = getItems({{0,0}, {1,0}, {0,2}, {1,2}}); + + // Remove the row in the center, and verify that the rows + // above and below didn't change delegate items. + model.removeRow(1); + WAIT_UNTIL_POLISHED; + + QCOMPARE(getItems({{0,0}, {1,0}, {0,1}, {1,1}}), initialItems); + verifyContextProperties({{0,0,0}, {2,0,1}, {1,1,0}, {3,1,1}}); + verifyInitialItemsNotPooled(); + + // Insert a row in the center, and then verify that the rows + // above and below didn't change delegate items. + model.insertRow(1); + WAIT_UNTIL_POLISHED; + + QCOMPARE(getItems({{0,0}, {1,0}, {0,2}, {1,2}}), initialItems); + verifyContextProperties({{0,0,0}, {3,0,1}, {2,2,0}, {5,2,1}}); + verifyInitialItemsNotPooled(); +} + QTEST_MAIN(tst_QQuickTableView) #include "tst_qquicktableview.moc" |
