fix: don't recreate all delegates on model change

This commit is contained in:
2 * r + 2 * t 2026-04-03 02:55:04 +11:00
parent 9e35e93f76
commit 2acd8f13f8
2 changed files with 122 additions and 21 deletions

View file

@ -375,6 +375,18 @@ void LazyListView::updatePolish() {
entry.item->setY(targetY); entry.item->setY(targetY);
} }
} }
// Flush delegate pool — any delegates not reclaimed are truly removed
for (auto& pooled : m_delegatePool) {
pooled.pendingRemoval = true;
if (m_removeDuration > 0 && pooled.item) {
startRemoveAnimation(pooled);
m_dyingDelegates.append(std::move(pooled));
} else {
destroyDelegate(pooled);
}
}
m_delegatePool.clear();
} }
// --- Layout Engine --- // --- Layout Engine ---
@ -492,6 +504,27 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
if (!m_delegate || !m_model) if (!m_delegate || !m_model)
return entry; return entry;
// Try to reclaim a delegate from the pool (reuse after remove+insert cycle)
const auto roleNames = m_model->roleNames();
const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key();
const auto targetData = m_model->data(m_model->index(modelIndex, 0), role);
for (auto it = m_delegatePool.begin(); it != m_delegatePool.end(); ++it) {
if (!it->item)
continue;
const auto poolData = it->item->property("modelData");
if (poolData == targetData) {
entry = std::move(*it);
m_delegatePool.erase(it);
entry.modelIndex = modelIndex;
entry.pendingRemoval = false;
updateDelegateData(entry);
entry.item->setParentItem(this);
entry.item->setWidth(width());
return entry;
}
}
// Use the delegate component's creation context so the delegate // Use the delegate component's creation context so the delegate
// can access ids and properties from the scope where it was defined. // can access ids and properties from the scope where it was defined.
auto* compContext = m_delegate->creationContext(); auto* compContext = m_delegate->creationContext();
@ -502,7 +535,6 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
entry.context = new QQmlContext(parentContext, this); entry.context = new QQmlContext(parentContext, this);
// Build property map for both context properties and initial properties // Build property map for both context properties and initial properties
const auto roleNames = m_model->roleNames();
const auto index = m_model->index(modelIndex, 0); const auto index = m_model->index(modelIndex, 0);
QVariantMap initialProps; QVariantMap initialProps;
@ -520,7 +552,6 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
// Provide modelData for single-role models or if not already provided by role names // Provide modelData for single-role models or if not already provided by role names
if (!hasModelData) { if (!hasModelData) {
const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key();
const auto value = m_model->data(index, role); const auto value = m_model->data(index, role);
entry.context->setContextProperty(QStringLiteral("modelData"), value); entry.context->setContextProperty(QStringLiteral("modelData"), value);
initialProps.insert(QStringLiteral("modelData"), value); initialProps.insert(QStringLiteral("modelData"), value);
@ -649,7 +680,11 @@ void LazyListView::connectModel() {
connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved),
connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged),
connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset),
connect(m_model, &QAbstractItemModel::layoutChanged, this, &LazyListView::onModelReset), connect(m_model, &QAbstractItemModel::layoutChanged, this, [this] {
for (auto& entry : m_delegates)
updateDelegateData(entry);
polish();
}),
connect(m_model, &QObject::destroyed, this, connect(m_model, &QObject::destroyed, this,
[this] { [this] {
m_model = nullptr; m_model = nullptr;
@ -675,6 +710,10 @@ void LazyListView::resetContent() {
destroyDelegate(entry); destroyDelegate(entry);
m_dyingDelegates.clear(); m_dyingDelegates.clear();
for (auto& entry : m_delegatePool)
destroyDelegate(entry);
m_delegatePool.clear();
if (m_activeAnimations != 0) { if (m_activeAnimations != 0) {
m_activeAnimations = 0; m_activeAnimations = 0;
emit settledChanged(); emit settledChanged();
@ -706,7 +745,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last
return; return;
const int insertCount = last - first + 1; const int insertCount = last - first + 1;
// Insert new layout records // Insert new layout records
m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false }); m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false });
@ -735,20 +773,14 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first,
if (parent.isValid()) if (parent.isValid())
return; return;
// Start remove animations for visible delegates being removed // Pool removed delegates — they may be reused if the model re-inserts the same data
for (int i = first; i <= last; ++i) { for (int i = first; i <= last; ++i) {
if (!m_delegates.contains(i)) if (!m_delegates.contains(i))
continue; continue;
auto entry = m_delegates.take(i); auto entry = m_delegates.take(i);
entry.pendingRemoval = true; stopAnimation(entry);
m_delegatePool.append(std::move(entry));
if (m_removeDuration > 0 && entry.item) {
startRemoveAnimation(entry);
m_dyingDelegates.append(std::move(entry));
} else {
destroyDelegate(entry);
}
} }
} }
@ -785,15 +817,48 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last)
polish(); polish();
} }
void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end,
Q_UNUSED(parent) const QModelIndex& destination, int row) {
Q_UNUSED(start) if (parent.isValid() || destination.isValid())
Q_UNUSED(end) return;
Q_UNUSED(destination)
Q_UNUSED(row)
// Full reset for moves — complex index remapping const int count = end - start + 1;
onModelReset(); const int dest = row > start ? row - count : row;
// Reorder layout records
QVector<ItemRecord> moved;
moved.reserve(count);
for (int i = start; i <= end; ++i)
moved.append(m_layout[i]);
m_layout.remove(start, count);
for (int i = 0; i < count; ++i)
m_layout.insert(dest + i, moved[i]);
// Remap delegate indices to match new model order
QHash<int, DelegateEntry> remapped;
for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) {
int oldIdx = it.key();
int newIdx = oldIdx;
if (oldIdx >= start && oldIdx <= end) {
newIdx = dest + (oldIdx - start);
} else {
if (oldIdx > end)
newIdx -= count;
if (newIdx >= dest)
newIdx += count;
}
auto entry = std::move(it.value());
entry.modelIndex = newIdx;
if (entry.context)
entry.context->setContextProperty(QStringLiteral("index"), newIdx);
remapped.insert(newIdx, std::move(entry));
}
m_delegates = std::move(remapped);
m_animateDisplacement = true;
polish();
} }
void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList<int>& roles) { void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList<int>& roles) {
@ -809,6 +874,41 @@ void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex&
} }
void LazyListView::onModelReset() { void LazyListView::onModelReset() {
if (!m_model) {
resetContent();
return;
}
const int newRows = m_model->rowCount();
const int oldRows = static_cast<int>(m_layout.size());
// Check if the model data actually changed
if (newRows == oldRows) {
const auto roleNames = m_model->roleNames();
const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key();
bool changed = false;
for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) {
if (!it->item || it.key() >= newRows) {
changed = true;
break;
}
const auto newData = m_model->data(m_model->index(it.key(), 0), role);
const auto oldData = it->item->property("modelData");
if (newData != oldData) {
changed = true;
break;
}
}
if (!changed) {
// Model content unchanged, just refresh delegate data
for (auto& entry : m_delegates)
updateDelegateData(entry);
return;
}
}
resetContent(); resetContent();
} }

View file

@ -256,6 +256,7 @@ private:
QVector<ItemRecord> m_layout; QVector<ItemRecord> m_layout;
QHash<int, DelegateEntry> m_delegates; QHash<int, DelegateEntry> m_delegates;
QVector<DelegateEntry> m_dyingDelegates; QVector<DelegateEntry> m_dyingDelegates;
QVector<DelegateEntry> m_delegatePool;
int m_activeAnimations = 0; int m_activeAnimations = 0;
bool m_componentComplete = false; bool m_componentComplete = false;