fix: model change sigabrt crash + resize anim
This commit is contained in:
parent
736eda6715
commit
9e35e93f76
3 changed files with 137 additions and 82 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LazyListViewAttached*>(
|
||||
qmlAttachedPropertiesObject<LazyListView>(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<int> 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<int>(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<int>(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<LazyListViewAttached*>(
|
||||
qmlAttachedPropertiesObject<LazyListView>(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<int>(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<int, qreal> 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<int, qreal> 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) {
|
||||
|
|
|
|||
|
|
@ -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<int, int> 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<int> m_pendingAddAnimations;
|
||||
|
||||
QList<QMetaObject::Connection> m_modelConnections;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue