From 9e35e93f76ab0510c5f136b06521c150d672e674 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:27:53 +1100 Subject: [PATCH] fix: model change sigabrt crash + resize anim --- modules/sidebar/NotifDockList.qml | 10 +- .../src/Caelestia/Components/lazylistview.cpp | 183 +++++++++++------- .../src/Caelestia/Components/lazylistview.hpp | 26 ++- 3 files changed, 137 insertions(+), 82 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index c7d1ebaf..f2dea1c2 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -72,7 +72,8 @@ LazyListView { } } - implicitHeight: closed ? 0 : notifInner.implicitHeight + LazyListView.preferredHeight: closed ? 0 : notifInner.implicitHeight + implicitHeight: notifInner.implicitHeight hoverEnabled: true cursorShape: pressed ? Qt.ClosedHandCursor : undefined @@ -119,13 +120,6 @@ LazyListView { easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } } } } diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 126c1202..ea059a10 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -5,12 +5,34 @@ namespace caelestia::components { +// --- LazyListViewAttached --- + +LazyListViewAttached::LazyListViewAttached(QObject* parent) + : QObject(parent) {} + +qreal LazyListViewAttached::preferredHeight() const { + return m_preferredHeight; +} + +void LazyListViewAttached::setPreferredHeight(qreal height) { + if (qFuzzyCompare(m_preferredHeight, height)) + return; + m_preferredHeight = height; + emit preferredHeightChanged(); +} + +// --- LazyListView --- + LazyListView::LazyListView(QQuickItem* parent) : QQuickItem(parent) { setFlag(ItemHasContents, false); setClip(true); } +LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) { + return new LazyListViewAttached(object); +} + LazyListView::~LazyListView() { for (auto& entry : m_delegates) destroyDelegate(entry); @@ -154,6 +176,18 @@ void LazyListView::untrackHeight(qreal height) { --m_knownHeightCount; } +qreal LazyListView::delegateHeight(QQuickItem* item) { + if (!item) + return 0; + + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(item, false)); + if (attached && attached->preferredHeight() >= 0) + return attached->preferredHeight(); + + return item->implicitHeight(); +} + // --- Add Animation --- int LazyListView::addDuration() const { @@ -310,7 +344,37 @@ void LazyListView::updatePolish() { relayout(); syncDelegates(); - positionDelegates(); + + // Animate newly created delegates that were pending add animation + QSet pendingAdds; + m_pendingAddAnimations.swap(pendingAdds); + for (int idx : std::as_const(pendingAdds)) { + if (m_delegates.contains(idx) && m_addDuration > 0) + startAddAnimation(m_delegates[idx]); + } + + // Position delegates, animating displacement if a model change occurred + const bool animate = m_animateDisplacement; + m_animateDisplacement = false; + + for (auto& entry : m_delegates) { + if (!entry.item || entry.pendingRemoval || entry.animation) + continue; + + const int idx = entry.modelIndex; + if (idx < 0 || idx >= static_cast(m_layout.size())) + continue; + + const qreal targetY = m_layout[idx].targetY - m_contentY; + const qreal currentY = entry.item->y(); + + if (animate && !qFuzzyCompare(currentY, targetY) && m_moveDuration > 0 + && !pendingAdds.contains(idx)) { + startMoveAnimation(entry, currentY); + } else if (!entry.animation) { + entry.item->setY(targetY); + } + } } // --- Layout Engine --- @@ -408,8 +472,8 @@ void LazyListView::syncDelegates() { auto entry = createDelegate(i); if (entry.item) { - // Measure height - const qreal h = entry.item->implicitHeight(); + // Measure height (prefer attached preferredHeight, fall back to implicitHeight) + const qreal h = delegateHeight(entry.item); if (h > 0 && !m_layout[i].heightKnown) { m_layout[i].height = h; m_layout[i].heightKnown = true; @@ -479,14 +543,14 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.item->setWidth(width()); m_delegate->completeCreate(); - // Watch for height changes - connect(entry.item, &QQuickItem::implicitHeightChanged, this, [this, modelIndex] { + // Shared height-change handler + auto onHeightChanged = [this, modelIndex] { if (!m_delegates.contains(modelIndex)) return; auto& e = m_delegates[modelIndex]; if (!e.item) return; - const qreal h = e.item->implicitHeight(); + const qreal h = delegateHeight(e.item); if (modelIndex < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[modelIndex].height, h)) { const qreal oldH = m_layout[modelIndex].height; const bool wasKnown = m_layout[modelIndex].heightKnown; @@ -497,20 +561,45 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { trackHeight(h); polish(); } - }); + }; + + // Watch implicitHeight as fallback + connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); + + // Watch attached preferredHeight if the delegate uses it + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(entry.item, false)); + if (attached) { + entry.attachedConnection = connect(attached, &LazyListViewAttached::preferredHeightChanged, + this, onHeightChanged); + } return entry; } void LazyListView::destroyDelegate(DelegateEntry& entry) { if (entry.animation) { + // Disconnect before stopping to prevent re-entrant onAnimationFinished + disconnect(entry.animation, &QAbstractAnimation::finished, + this, &LazyListView::onAnimationFinished); entry.animation->stop(); entry.animation = nullptr; + --m_activeAnimations; + if (m_activeAnimations == 0) + emit settledChanged(); + } + if (entry.attachedConnection) + disconnect(entry.attachedConnection); + if (entry.item) { + entry.item->setParentItem(nullptr); + entry.item->setVisible(false); + entry.item->deleteLater(); + entry.item = nullptr; + } + if (entry.context) { + entry.context->deleteLater(); + entry.context = nullptr; } - delete entry.item; - entry.item = nullptr; - delete entry.context; - entry.context = nullptr; } void LazyListView::updateDelegateData(DelegateEntry& entry) { @@ -547,23 +636,6 @@ void LazyListView::updateDelegateData(DelegateEntry& entry) { } } -void LazyListView::positionDelegates() { - for (auto& entry : m_delegates) { - if (!entry.item || entry.pendingRemoval) - continue; - - // Don't reposition if a move animation is running on this delegate - if (entry.animation) - continue; - - const int idx = entry.modelIndex; - if (idx < 0 || idx >= static_cast(m_layout.size())) - continue; - - entry.item->setY(m_layout[idx].targetY - m_contentY); - } -} - // --- Model Connection --- void LazyListView::connectModel() { @@ -608,9 +680,11 @@ void LazyListView::resetContent() { emit settledChanged(); } - // Reset height tracking + // Reset pending state m_knownHeightSum = 0; m_knownHeightCount = 0; + m_pendingAddAnimations.clear(); + m_animateDisplacement = false; // Rebuild layout from model m_layout.clear(); @@ -633,13 +707,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last const int insertCount = last - first + 1; - // Capture old positions of existing delegates for move animation - QHash oldPositions; - for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (it.key() >= first) - oldPositions.insert(it.key(), m_layout[it.key()].targetY); - } - // Insert new layout records m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false }); @@ -655,26 +722,13 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last } m_delegates = std::move(shifted); - relayout(); - syncDelegates(); - positionDelegates(); - - // Animate new items - for (int i = first; i <= last; ++i) { - if (m_delegates.contains(i) && m_addDuration > 0) - startAddAnimation(m_delegates[i]); - } - - // Animate displaced items - for (auto it = oldPositions.begin(); it != oldPositions.end(); ++it) { - const int newIdx = it.key() + insertCount; - if (m_delegates.contains(newIdx) && m_moveDuration > 0) { - const qreal oldY = it.value() - m_contentY; - startMoveAnimation(m_delegates[newIdx], oldY); - } - } + // Queue add animations and mark displacement + for (int i = first; i <= last; ++i) + m_pendingAddAnimations.insert(i); + m_animateDisplacement = true; emit countChanged(); + polish(); } void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { @@ -704,13 +758,6 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) const int removeCount = last - first + 1; - // Capture old positions for displaced animation - QHash oldPositions; - for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (it.key() > last) - oldPositions.insert(it.key(), m_layout[it.key()].targetY); - } - // Untrack known heights being removed for (int i = first; i <= last; ++i) { if (m_layout[i].heightKnown) @@ -732,20 +779,10 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) } m_delegates = std::move(shifted); - relayout(); - syncDelegates(); - positionDelegates(); - - // Animate displaced items - for (auto it = oldPositions.begin(); it != oldPositions.end(); ++it) { - const int newIdx = it.key() - removeCount; - if (m_delegates.contains(newIdx) && m_moveDuration > 0) { - const qreal oldY = it.value() - m_contentY; - startMoveAnimation(m_delegates[newIdx], oldY); - } - } + m_animateDisplacement = true; emit countChanged(); + polish(); } void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index a310f96f..fdc0907f 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -14,9 +14,28 @@ namespace caelestia::components { +class LazyListViewAttached : public QObject { + Q_OBJECT + + Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) + +public: + explicit LazyListViewAttached(QObject* parent = nullptr); + + [[nodiscard]] qreal preferredHeight() const; + void setPreferredHeight(qreal height); + +signals: + void preferredHeightChanged(); + +private: + qreal m_preferredHeight = -1; +}; + class LazyListView : public QQuickItem { Q_OBJECT QML_ELEMENT + QML_ATTACHED(LazyListViewAttached) // Model & Delegate Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged) @@ -59,6 +78,8 @@ public: explicit LazyListView(QQuickItem* parent = nullptr); ~LazyListView() override; + static LazyListViewAttached* qmlAttachedProperties(QObject* object); + // Model & Delegate [[nodiscard]] QAbstractItemModel* model() const; void setModel(QAbstractItemModel* model); @@ -167,6 +188,7 @@ private: QQmlContext* context = nullptr; bool pendingRemoval = false; QParallelAnimationGroup* animation = nullptr; + QMetaObject::Connection attachedConnection; }; // Layout @@ -174,6 +196,7 @@ private: [[nodiscard]] std::pair computeVisibleRange() const; [[nodiscard]] QRectF effectiveViewport() const; [[nodiscard]] qreal effectiveEstimatedHeight() const; + [[nodiscard]] static qreal delegateHeight(QQuickItem* item); void trackHeight(qreal height); void untrackHeight(qreal height); @@ -182,7 +205,6 @@ private: DelegateEntry createDelegate(int modelIndex); void destroyDelegate(DelegateEntry& entry); void updateDelegateData(DelegateEntry& entry); - void positionDelegates(); // Model connection void connectModel(); @@ -237,6 +259,8 @@ private: int m_activeAnimations = 0; bool m_componentComplete = false; + bool m_animateDisplacement = false; + QSet m_pendingAddAnimations; QList m_modelConnections; };