diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index ea059a10..d1e2570c 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -375,6 +375,18 @@ void LazyListView::updatePolish() { 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 --- @@ -492,6 +504,27 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (!m_delegate || !m_model) 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 // can access ids and properties from the scope where it was defined. auto* compContext = m_delegate->creationContext(); @@ -502,7 +535,6 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.context = new QQmlContext(parentContext, this); // Build property map for both context properties and initial properties - const auto roleNames = m_model->roleNames(); const auto index = m_model->index(modelIndex, 0); 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 if (!hasModelData) { - const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); const auto value = m_model->data(index, role); entry.context->setContextProperty(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::dataChanged, this, &LazyListView::onDataChanged), 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, [this] { m_model = nullptr; @@ -675,6 +710,10 @@ void LazyListView::resetContent() { destroyDelegate(entry); m_dyingDelegates.clear(); + for (auto& entry : m_delegatePool) + destroyDelegate(entry); + m_delegatePool.clear(); + if (m_activeAnimations != 0) { m_activeAnimations = 0; emit settledChanged(); @@ -706,7 +745,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last return; const int insertCount = last - first + 1; - // Insert new layout records m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false }); @@ -735,20 +773,14 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, if (parent.isValid()) 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) { if (!m_delegates.contains(i)) continue; auto entry = m_delegates.take(i); - entry.pendingRemoval = true; - - if (m_removeDuration > 0 && entry.item) { - startRemoveAnimation(entry); - m_dyingDelegates.append(std::move(entry)); - } else { - destroyDelegate(entry); - } + stopAnimation(entry); + m_delegatePool.append(std::move(entry)); } } @@ -785,15 +817,48 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) polish(); } -void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { - Q_UNUSED(parent) - Q_UNUSED(start) - Q_UNUSED(end) - Q_UNUSED(destination) - Q_UNUSED(row) +void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, + const QModelIndex& destination, int row) { + if (parent.isValid() || destination.isValid()) + return; - // Full reset for moves — complex index remapping - onModelReset(); + const int count = end - start + 1; + const int dest = row > start ? row - count : row; + + // Reorder layout records + QVector 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 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& roles) { @@ -809,6 +874,41 @@ void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& } void LazyListView::onModelReset() { + if (!m_model) { + resetContent(); + return; + } + + const int newRows = m_model->rowCount(); + const int oldRows = static_cast(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(); } diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index fdc0907f..c4770552 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -256,6 +256,7 @@ private: QVector m_layout; QHash m_delegates; QVector m_dyingDelegates; + QVector m_delegatePool; int m_activeAnimations = 0; bool m_componentComplete = false;