fix: don't recreate all delegates on model change
This commit is contained in:
parent
9e35e93f76
commit
2acd8f13f8
2 changed files with 122 additions and 21 deletions
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue