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
|
hoverEnabled: true
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
||||||
|
|
@ -119,13 +120,6 @@ LazyListView {
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
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 {
|
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)
|
LazyListView::LazyListView(QQuickItem* parent)
|
||||||
: QQuickItem(parent) {
|
: QQuickItem(parent) {
|
||||||
setFlag(ItemHasContents, false);
|
setFlag(ItemHasContents, false);
|
||||||
setClip(true);
|
setClip(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) {
|
||||||
|
return new LazyListViewAttached(object);
|
||||||
|
}
|
||||||
|
|
||||||
LazyListView::~LazyListView() {
|
LazyListView::~LazyListView() {
|
||||||
for (auto& entry : m_delegates)
|
for (auto& entry : m_delegates)
|
||||||
destroyDelegate(entry);
|
destroyDelegate(entry);
|
||||||
|
|
@ -154,6 +176,18 @@ void LazyListView::untrackHeight(qreal height) {
|
||||||
--m_knownHeightCount;
|
--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 ---
|
// --- Add Animation ---
|
||||||
|
|
||||||
int LazyListView::addDuration() const {
|
int LazyListView::addDuration() const {
|
||||||
|
|
@ -310,7 +344,37 @@ void LazyListView::updatePolish() {
|
||||||
|
|
||||||
relayout();
|
relayout();
|
||||||
syncDelegates();
|
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 ---
|
// --- Layout Engine ---
|
||||||
|
|
@ -408,8 +472,8 @@ void LazyListView::syncDelegates() {
|
||||||
|
|
||||||
auto entry = createDelegate(i);
|
auto entry = createDelegate(i);
|
||||||
if (entry.item) {
|
if (entry.item) {
|
||||||
// Measure height
|
// Measure height (prefer attached preferredHeight, fall back to implicitHeight)
|
||||||
const qreal h = entry.item->implicitHeight();
|
const qreal h = delegateHeight(entry.item);
|
||||||
if (h > 0 && !m_layout[i].heightKnown) {
|
if (h > 0 && !m_layout[i].heightKnown) {
|
||||||
m_layout[i].height = h;
|
m_layout[i].height = h;
|
||||||
m_layout[i].heightKnown = true;
|
m_layout[i].heightKnown = true;
|
||||||
|
|
@ -479,14 +543,14 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
|
||||||
entry.item->setWidth(width());
|
entry.item->setWidth(width());
|
||||||
m_delegate->completeCreate();
|
m_delegate->completeCreate();
|
||||||
|
|
||||||
// Watch for height changes
|
// Shared height-change handler
|
||||||
connect(entry.item, &QQuickItem::implicitHeightChanged, this, [this, modelIndex] {
|
auto onHeightChanged = [this, modelIndex] {
|
||||||
if (!m_delegates.contains(modelIndex))
|
if (!m_delegates.contains(modelIndex))
|
||||||
return;
|
return;
|
||||||
auto& e = m_delegates[modelIndex];
|
auto& e = m_delegates[modelIndex];
|
||||||
if (!e.item)
|
if (!e.item)
|
||||||
return;
|
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)) {
|
if (modelIndex < static_cast<int>(m_layout.size()) && !qFuzzyCompare(m_layout[modelIndex].height, h)) {
|
||||||
const qreal oldH = m_layout[modelIndex].height;
|
const qreal oldH = m_layout[modelIndex].height;
|
||||||
const bool wasKnown = m_layout[modelIndex].heightKnown;
|
const bool wasKnown = m_layout[modelIndex].heightKnown;
|
||||||
|
|
@ -497,20 +561,45 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
|
||||||
trackHeight(h);
|
trackHeight(h);
|
||||||
polish();
|
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;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LazyListView::destroyDelegate(DelegateEntry& entry) {
|
void LazyListView::destroyDelegate(DelegateEntry& entry) {
|
||||||
if (entry.animation) {
|
if (entry.animation) {
|
||||||
|
// Disconnect before stopping to prevent re-entrant onAnimationFinished
|
||||||
|
disconnect(entry.animation, &QAbstractAnimation::finished,
|
||||||
|
this, &LazyListView::onAnimationFinished);
|
||||||
entry.animation->stop();
|
entry.animation->stop();
|
||||||
entry.animation = nullptr;
|
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) {
|
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 ---
|
// --- Model Connection ---
|
||||||
|
|
||||||
void LazyListView::connectModel() {
|
void LazyListView::connectModel() {
|
||||||
|
|
@ -608,9 +680,11 @@ void LazyListView::resetContent() {
|
||||||
emit settledChanged();
|
emit settledChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset height tracking
|
// Reset pending state
|
||||||
m_knownHeightSum = 0;
|
m_knownHeightSum = 0;
|
||||||
m_knownHeightCount = 0;
|
m_knownHeightCount = 0;
|
||||||
|
m_pendingAddAnimations.clear();
|
||||||
|
m_animateDisplacement = false;
|
||||||
|
|
||||||
// Rebuild layout from model
|
// Rebuild layout from model
|
||||||
m_layout.clear();
|
m_layout.clear();
|
||||||
|
|
@ -633,13 +707,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last
|
||||||
|
|
||||||
const int insertCount = last - first + 1;
|
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
|
// Insert new layout records
|
||||||
m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false });
|
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);
|
m_delegates = std::move(shifted);
|
||||||
|
|
||||||
relayout();
|
// Queue add animations and mark displacement
|
||||||
syncDelegates();
|
for (int i = first; i <= last; ++i)
|
||||||
positionDelegates();
|
m_pendingAddAnimations.insert(i);
|
||||||
|
m_animateDisplacement = true;
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit countChanged();
|
emit countChanged();
|
||||||
|
polish();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) {
|
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;
|
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
|
// Untrack known heights being removed
|
||||||
for (int i = first; i <= last; ++i) {
|
for (int i = first; i <= last; ++i) {
|
||||||
if (m_layout[i].heightKnown)
|
if (m_layout[i].heightKnown)
|
||||||
|
|
@ -732,20 +779,10 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last)
|
||||||
}
|
}
|
||||||
m_delegates = std::move(shifted);
|
m_delegates = std::move(shifted);
|
||||||
|
|
||||||
relayout();
|
m_animateDisplacement = true;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit countChanged();
|
emit countChanged();
|
||||||
|
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, const QModelIndex& destination, int row) {
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,28 @@
|
||||||
|
|
||||||
namespace caelestia::components {
|
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 {
|
class LazyListView : public QQuickItem {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
QML_ATTACHED(LazyListViewAttached)
|
||||||
|
|
||||||
// Model & Delegate
|
// Model & Delegate
|
||||||
Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged)
|
Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged)
|
||||||
|
|
@ -59,6 +78,8 @@ public:
|
||||||
explicit LazyListView(QQuickItem* parent = nullptr);
|
explicit LazyListView(QQuickItem* parent = nullptr);
|
||||||
~LazyListView() override;
|
~LazyListView() override;
|
||||||
|
|
||||||
|
static LazyListViewAttached* qmlAttachedProperties(QObject* object);
|
||||||
|
|
||||||
// Model & Delegate
|
// Model & Delegate
|
||||||
[[nodiscard]] QAbstractItemModel* model() const;
|
[[nodiscard]] QAbstractItemModel* model() const;
|
||||||
void setModel(QAbstractItemModel* model);
|
void setModel(QAbstractItemModel* model);
|
||||||
|
|
@ -167,6 +188,7 @@ private:
|
||||||
QQmlContext* context = nullptr;
|
QQmlContext* context = nullptr;
|
||||||
bool pendingRemoval = false;
|
bool pendingRemoval = false;
|
||||||
QParallelAnimationGroup* animation = nullptr;
|
QParallelAnimationGroup* animation = nullptr;
|
||||||
|
QMetaObject::Connection attachedConnection;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
|
|
@ -174,6 +196,7 @@ private:
|
||||||
[[nodiscard]] std::pair<int, int> computeVisibleRange() const;
|
[[nodiscard]] std::pair<int, int> computeVisibleRange() const;
|
||||||
[[nodiscard]] QRectF effectiveViewport() const;
|
[[nodiscard]] QRectF effectiveViewport() const;
|
||||||
[[nodiscard]] qreal effectiveEstimatedHeight() const;
|
[[nodiscard]] qreal effectiveEstimatedHeight() const;
|
||||||
|
[[nodiscard]] static qreal delegateHeight(QQuickItem* item);
|
||||||
void trackHeight(qreal height);
|
void trackHeight(qreal height);
|
||||||
void untrackHeight(qreal height);
|
void untrackHeight(qreal height);
|
||||||
|
|
||||||
|
|
@ -182,7 +205,6 @@ private:
|
||||||
DelegateEntry createDelegate(int modelIndex);
|
DelegateEntry createDelegate(int modelIndex);
|
||||||
void destroyDelegate(DelegateEntry& entry);
|
void destroyDelegate(DelegateEntry& entry);
|
||||||
void updateDelegateData(DelegateEntry& entry);
|
void updateDelegateData(DelegateEntry& entry);
|
||||||
void positionDelegates();
|
|
||||||
|
|
||||||
// Model connection
|
// Model connection
|
||||||
void connectModel();
|
void connectModel();
|
||||||
|
|
@ -237,6 +259,8 @@ private:
|
||||||
|
|
||||||
int m_activeAnimations = 0;
|
int m_activeAnimations = 0;
|
||||||
bool m_componentComplete = false;
|
bool m_componentComplete = false;
|
||||||
|
bool m_animateDisplacement = false;
|
||||||
|
QSet<int> m_pendingAddAnimations;
|
||||||
|
|
||||||
QList<QMetaObject::Connection> m_modelConnections;
|
QList<QMetaObject::Connection> m_modelConnections;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue