#include "chatlistview.hpp" #include #include #include #include #include #include namespace { constexpr int ASYNC_BATCH_CREATE = 2; constexpr int ASYNC_BATCH_DESTROY = 4; constexpr int RESTORE_QUIET_MS = 150; constexpr int MEASURE_BATCH = 4; constexpr int MEASURE_SETTLE_MS = 100; constexpr int MEASURE_DEBOUNCE_MS = 150; } // namespace namespace ZShell::components { ChatListViewAttached::ChatListViewAttached(QObject* parent) : QObject(parent) {} qreal ChatListViewAttached::preferredHeight() const { return m_preferredHeight; } void ChatListViewAttached::setPreferredHeight(qreal height) { if (qFuzzyCompare(m_preferredHeight + 1.0, height + 1.0)) return; m_preferredHeight = height; emit preferredHeightChanged(); } qreal ChatListViewAttached::visibleHeight() const { return m_visibleHeight; } void ChatListViewAttached::setVisibleHeight(qreal height) { if (qFuzzyCompare(m_visibleHeight + 1.0, height + 1.0)) return; m_visibleHeight = height; emit visibleHeightChanged(); } bool ChatListViewAttached::ready() const { return m_ready; } void ChatListViewAttached::setReady(bool ready) { if (m_ready == ready) return; m_ready = ready; emit readyChanged(); } bool ChatListViewAttached::adding() const { return m_adding; } void ChatListViewAttached::setAdding(bool adding) { if (m_adding == adding) return; m_adding = adding; emit addingChanged(); } bool ChatListViewAttached::removing() const { return m_removing; } void ChatListViewAttached::setRemoving(bool removing) { if (m_removing == removing) return; m_removing = removing; emit removingChanged(); } bool ChatListViewAttached::trackViewport() const { return m_trackViewport; } void ChatListViewAttached::setTrackViewport(bool track) { if (m_trackViewport == track) return; m_trackViewport = track; emit trackViewportChanged(); } ChatListView::ChatListView(QQuickItem* parent) : QQuickItem(parent) { m_measureTimer = new QTimer(this); m_measureTimer->setSingleShot(true); m_measureTimer->setInterval(MEASURE_DEBOUNCE_MS); connect(m_measureTimer, &QTimer::timeout, this, [this] { buildMeasureQueue(); polish(); }); } ChatListViewAttached* ChatListView::qmlAttachedProperties(QObject* object) { return new ChatListViewAttached(object); } ChatListView::~ChatListView() { for (auto& entry : m_delegates) destroyDelegate(entry); for (auto& entry : m_dyingDelegates) destroyDelegate(entry); } QAbstractItemModel* ChatListView::model() const { return m_model; } void ChatListView::setModel(QAbstractItemModel* model) { if (m_model == model) return; if (m_model) disconnectModel(); m_model = model; if (m_model) connectModel(); resetContent(); emit modelChanged(); } QQmlComponent* ChatListView::delegate() const { return m_delegate; } void ChatListView::setDelegate(QQmlComponent* delegate) { if (m_delegate == delegate) return; m_delegate = delegate; resetContent(); emit delegateChanged(); } qreal ChatListView::spacing() const { return m_spacing; } void ChatListView::setSpacing(qreal spacing) { if (qFuzzyCompare(m_spacing, spacing)) return; m_spacing = spacing; emit spacingChanged(); polish(); } qreal ChatListView::contentHeight() const { return m_contentHeight; } qreal ChatListView::layoutHeight() const { return m_layoutHeight; } qreal ChatListView::contentY() const { return m_contentY; } void ChatListView::setContentY(qreal contentY) { if (qFuzzyCompare(m_contentY, contentY)) return; m_contentY = contentY; emit contentYChanged(); polish(); } QRectF ChatListView::viewport() const { return m_viewport; } void ChatListView::setViewport(const QRectF& viewport) { if (m_viewport == viewport) return; m_viewport = viewport; emit viewportChanged(); if (m_useCustomViewport) polish(); } bool ChatListView::useCustomViewport() const { return m_useCustomViewport; } void ChatListView::setUseCustomViewport(bool use) { if (m_useCustomViewport == use) return; m_useCustomViewport = use; emit useCustomViewportChanged(); polish(); } qreal ChatListView::cacheBuffer() const { return m_cacheBuffer; } void ChatListView::setCacheBuffer(qreal buffer) { if (qFuzzyCompare(m_cacheBuffer, buffer)) return; m_cacheBuffer = buffer; emit cacheBufferChanged(); polish(); } qreal ChatListView::estimatedHeight() const { return m_estimatedHeight; } void ChatListView::setEstimatedHeight(qreal height) { if (qFuzzyCompare(m_estimatedHeight, height)) return; m_estimatedHeight = height; emit estimatedHeightChanged(); polish(); } bool ChatListView::asynchronous() const { return m_asynchronous; } void ChatListView::setAsynchronous(bool async) { if (m_asynchronous == async) return; m_asynchronous = async; emit asynchronousChanged(); polish(); } int ChatListView::removeDuration() const { return m_removeDuration; } void ChatListView::setRemoveDuration(int duration) { if (m_removeDuration == duration) return; m_removeDuration = duration; emit removeDurationChanged(); } int ChatListView::readyDelay() const { return m_readyDelay; } void ChatListView::setReadyDelay(int delay) { if (m_readyDelay == delay) return; m_readyDelay = delay; emit readyDelayChanged(); } int ChatListView::yAnimDuration() const { return m_yAnimDuration; } void ChatListView::setYAnimDuration(int duration) { if (m_yAnimDuration == duration) return; m_yAnimDuration = duration; emit yAnimDurationChanged(); } int ChatListView::count() const { return m_model ? m_model->rowCount() : 0; } bool ChatListView::event(QEvent* e) { return QQuickItem::event(e); } void ChatListView::componentComplete() { QQuickItem::componentComplete(); m_componentComplete = true; resetContent(); } void ChatListView::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry) { QQuickItem::geometryChange(newGeometry, oldGeometry); if (!m_componentComplete) return; if (!qFuzzyCompare(newGeometry.width(), oldGeometry.width())) { for (auto& entry : m_delegates) { if (entry.item) entry.item->setWidth(newGeometry.width()); } m_measureTimer->start(); } polish(); } void ChatListView::updatePolish() { if (!m_componentComplete || !m_model || !m_delegate) return; QList pendingIndices; for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) { if (it->pendingInsert && it->item) pendingIndices.append(it.key()); } for (int pendingIdx : pendingIndices) { auto& entry = m_delegates[pendingIdx]; if (m_readyDelay > 0) { if (!entry.readyDelayStarted) { entry.readyDelayStarted = true; auto* item = entry.item; QTimer::singleShot(m_readyDelay, this, [this, item] { auto indexIt = m_itemToIndex.find(item); if (indexIt == m_itemToIndex.end()) return; const int idx = indexIt.value(); auto it = m_delegates.find(idx); if (it == m_delegates.end() || it->item != item || !it->pendingInsert) return; it->pendingInsert = false; it->readyDelayStarted = false; if (idx >= 0 && idx < static_cast(m_layout.size())) { qreal visualY = 0; bool hasVisItem = false; for (int i = 0; i < static_cast(m_layout.size()); ++i) { qreal h = NAN; auto dit = m_delegates.find(i); if (dit != m_delegates.end() && dit->item) h = delegateVisibleHeight(dit->item); else h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); if (h > 0) { if (hasVisItem) visualY += m_spacing; hasVisItem = true; } if (i == idx) break; if (h > 0) visualY += h; } item->setY(visualY); } item->setVisible(true); auto* att = qobject_cast( qmlAttachedPropertiesObject(item, false)); if (att) { att->setAdding(false); att->setReady(true); } m_animateY = true; polish(); }); } continue; } entry.pendingInsert = false; entry.item->setVisible(true); auto* att = qobject_cast( qmlAttachedPropertiesObject(entry.item, false)); if (att) { att->setAdding(false); att->setReady(true); } } relayout(); syncDelegates(); pumpMeasureQueue(); for (auto& record : m_layout) record.isNew = false; const bool animate = m_animateY; m_animateY = false; const int layoutSize = static_cast(m_layout.size()); const qint64 now = m_clock.elapsed(); for (auto& entry : m_delegates) { if (!entry.item || entry.pendingRemoval || entry.pendingInsert) continue; const int idx = entry.modelIndex; if (idx < 0 || idx >= layoutSize) continue; if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height)) continue; const qreal target = m_layout[idx].targetY; const qreal cur = entry.item->y(); const bool inFlight = entry.hasPropY && qAbs(cur - entry.propY) > 0.5; const bool cooling = entry.hasPropY && (now - entry.lastPropTime) < m_yAnimDuration + 50; const bool needWrite = inFlight ? qAbs(target - entry.propY) > 0.25 : qAbs(cur - target) > 0.25; if (!needWrite) continue; if (animate || inFlight || cooling) { entry.item->setProperty("y", target); entry.propY = target; entry.hasPropY = true; entry.lastPropTime = now; } else { entry.item->setY(target); entry.hasPropY = false; } } applyAnchor(); } void ChatListView::relayout() { qreal y = 0; bool hasLayoutItem = false; for (auto& record : m_layout) { const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); if (layoutH > 0) { if (hasLayoutItem) y += m_spacing; hasLayoutItem = true; record.targetY = y; y += layoutH; } else { record.targetY = y; } } if (!qFuzzyCompare(m_layoutHeight + 1.0, y + 1.0)) { m_layoutHeight = y; emit layoutHeightChanged(); } qreal visY = 0; bool hasVisItem = false; for (int i = 0; i < static_cast(m_layout.size()); ++i) { qreal h = NAN; auto dit = m_delegates.find(i); if (dit != m_delegates.end() && dit->item) h = delegateVisibleHeight(dit->item); else h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); if (h > 0) { if (hasVisItem) visY += m_spacing; hasVisItem = true; visY += h; } } for (const auto& dying : std::as_const(m_dyingDelegates)) { if (!dying.item) continue; const qreal dyingH = delegateVisibleHeight(dying.item); if (dyingH > 0) visY = std::max(visY, dying.item->y() + dyingH); } if (!qFuzzyCompare(m_contentHeight + 1.0, visY + 1.0)) { m_contentHeight = visY; emit contentHeightChanged(); } } QRectF ChatListView::effectiveViewport() const { QRectF vp; if (m_useCustomViewport) vp = m_viewport; else vp = QRectF(0, m_contentY, width(), height()); if (!m_useCustomViewport && m_layoutHeight > 0) { const qreal top = std::min(vp.y(), m_layoutHeight); const qreal bottom = std::max(vp.y() + vp.height(), 0.0); if (bottom > top) vp = QRectF(vp.x(), top, vp.width(), bottom - top); } vp.adjust(0, -m_cacheBuffer, 0, m_cacheBuffer); if (m_layoutHeight > 0) { const qreal top = std::max(vp.y(), 0.0); const qreal bottom = std::min(vp.y() + vp.height(), m_layoutHeight); if (top < bottom) vp = QRectF(vp.x(), top, vp.width(), bottom - top); else return {}; } return vp; } std::pair ChatListView::computeVisibleRange() const { if (m_layout.isEmpty()) return {-1, -1}; const auto vp = effectiveViewport(); if (vp.isEmpty()) return {-1, -1}; const qreal vpTop = vp.y(); const qreal vpBottom = vp.y() + vp.height(); int lo = 0; int hi = static_cast(m_layout.size()) - 1; int first = static_cast(m_layout.size()); while (lo <= hi) { const int mid = lo + (hi - lo) / 2; const auto& record = m_layout[mid]; const qreal itemBottom = record.targetY + (record.heightKnown ? record.height : effectiveEstimatedHeight()); if (itemBottom >= vpTop) { first = mid; hi = mid - 1; } else { lo = mid + 1; } } if (first >= static_cast(m_layout.size())) return {-1, -1}; int last = first; for (int i = first; i < static_cast(m_layout.size()); ++i) { if (m_layout[i].targetY > vpBottom) break; last = i; } return {first, last}; } void ChatListView::syncDelegates() { const auto [first, last] = computeVisibleRange(); QSet visibleIndices; if (first >= 0) { for (int i = first; i <= last; ++i) visibleIndices.insert(i); } const auto vp = effectiveViewport(); QList toRemove; for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { if (it->measureOnly) continue; if (visibleIndices.contains(it.key())) continue; if (!it->item || vp.isEmpty()) { toRemove.append(it.key()); continue; } const qreal itemTop = it->item->y(); const qreal itemBottom = itemTop + delegateVisibleHeight(it->item); if (itemBottom < vp.top() || itemTop > vp.bottom()) toRemove.append(it.key()); } const int destroyBudget = m_asynchronous ? ASYNC_BATCH_DESTROY : static_cast(toRemove.size()); QVector removedEntries; removedEntries.reserve( std::min(destroyBudget, static_cast(toRemove.size()))); int destroyed = 0; for (int idx : toRemove) { if (destroyed >= destroyBudget) break; auto entry = m_delegates.take(idx); if (entry.item) m_itemToIndex.remove(entry.item); removedEntries.append(std::move(entry)); ++destroyed; } for (auto& entry : removedEntries) { destroyDelegate(entry); } QList toCreate; if (first >= 0) { for (int i = first; i <= last; ++i) { auto existing = m_delegates.find(i); if (existing != m_delegates.end()) { if (existing->measureOnly) { existing->measureOnly = false; polish(); } continue; } toCreate.append(i); } } const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast(toCreate.size()); int created = 0; for (int i : toCreate) { if (created >= createBudget) break; auto entry = createDelegate(i); if (entry.item) { entry.pendingInsert = true; entry.item->setY(m_layout[i].targetY); m_itemToIndex.insert(entry.item, i); m_delegates.insert(i, std::move(entry)); ++created; } } if (created > 0 || (m_asynchronous && (destroyed < static_cast(toRemove.size()) || created < static_cast(toCreate.size())))) polish(); } bool ChatListView::isStaleRecord(const ItemRecord& record, qreal viewWidth) { return record.heightKnown && record.measuredWidth >= 0 && qAbs(record.measuredWidth - viewWidth) > 1.0; } void ChatListView::buildMeasureQueue() { m_measureQueue.clear(); if (m_layout.isEmpty()) return; qreal center = 0; const auto vp = effectiveViewport(); if (!vp.isEmpty()) center = vp.center().y(); else if (m_layoutHeight > 0) center = m_layoutHeight / 2; for (int i = 0; i < static_cast(m_layout.size()); ++i) { if (!isStaleRecord(m_layout[i], width())) continue; if (m_delegates.contains(i)) continue; m_measureQueue.append(i); } const qreal c = center; std::stable_sort( m_measureQueue.begin(), m_measureQueue.end(), [this, c](int a, int b) { return qAbs(m_layout[a].targetY - c) < qAbs(m_layout[b].targetY - c); }); } void ChatListView::pumpMeasureQueue() { const qint64 now = m_clock.elapsed(); QList done; for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) { if (!it->measureOnly || !it->item) continue; if (!isDelegateReady(it->item)) continue; if (now - it->lastHeightChange < MEASURE_SETTLE_MS) continue; done.append(it.key()); } for (int idx : done) { auto entry = m_delegates.take(idx); if (entry.item) m_itemToIndex.remove(entry.item); destroyDelegate(entry); } int live = 0; for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) if (it->measureOnly) ++live; bool created = false; while (live < MEASURE_BATCH && !m_measureQueue.isEmpty()) { const int idx = m_measureQueue.takeFirst(); if (idx < 0 || idx >= static_cast(m_layout.size())) continue; if (!isStaleRecord(m_layout[idx], width())) continue; if (m_delegates.contains(idx)) continue; auto entry = createDelegate(idx); if (!entry.item) continue; entry.measureOnly = true; entry.pendingInsert = true; entry.lastHeightChange = now; entry.item->setY(m_layout[idx].targetY); m_itemToIndex.insert(entry.item, idx); m_delegates.insert(idx, std::move(entry)); ++live; created = true; } if (created || live > 0 || !m_measureQueue.isEmpty()) polish(); } ChatListView::DelegateEntry ChatListView::createDelegate(int modelIndex) { DelegateEntry entry; entry.modelIndex = modelIndex; if (!m_delegate || !m_model) return entry; const auto roleNames = m_model->roleNames(); auto* compContext = m_delegate->creationContext(); if (!compContext) compContext = qmlContext(this); if (!compContext) return entry; auto* obj = m_delegate->beginCreate(compContext); entry.item = qobject_cast(obj); if (!entry.item) { if (obj) m_delegate->completeCreate(); delete obj; return entry; } const auto index = m_model->index(modelIndex, 0); QVariantMap initialProps; bool hasModelData = false; for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { const auto name = QString::fromUtf8(it.value()); initialProps.insert(name, m_model->data(index, it.key())); if (name == QStringLiteral("modelData")) hasModelData = true; } initialProps.insert(QStringLiteral("index"), modelIndex); if (!hasModelData) { const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); initialProps.insert( QStringLiteral("modelData"), m_model->data(index, role)); } m_delegate->setInitialProperties(entry.item, initialProps); entry.item->setParentItem(this); entry.item->setWidth(width()); if (modelIndex < static_cast(m_layout.size()) && m_layout[modelIndex].isNew) { auto* addingAttached = qobject_cast( qmlAttachedPropertiesObject(entry.item, true)); if (addingAttached) addingAttached->setAdding(true); } m_delegate->completeCreate(); entry.item->setVisible(false); auto onHeightChanged = [this, item = entry.item] { if (!isDelegateReady(item)) return; auto indexIt = m_itemToIndex.find(item); if (indexIt == m_itemToIndex.end()) return; const int idx = indexIt.value(); auto delegateIt = m_delegates.find(idx); if (delegateIt == m_delegates.end() || delegateIt->item != item) return; const qreal h = delegateHeight(item); if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) { const qreal oldH = m_layout[idx].height; const bool wasKnown = m_layout[idx].heightKnown; m_layout[idx].height = h; m_layout[idx].heightKnown = true; m_layout[idx].measuredWidth = width(); if (wasKnown) untrackHeight(oldH); trackHeight(h); if (delegateIt->measureOnly) delegateIt->lastHeightChange = m_clock.elapsed(); if (!wasKnown) { m_animateY = true; } if (wasKnown) { auto* att = qobject_cast( qmlAttachedPropertiesObject(item, false)); if (att && att->trackViewport() && !m_anchorItem && m_layout[idx].targetY < m_viewport.y()) { bool suppressed = false; if (m_restoreActive && idx == m_restoreIndex) { if (m_clock.elapsed() < m_restoreQuietUntil) { suppressed = true; m_restoreQuietUntil = m_clock.elapsed() + RESTORE_QUIET_MS; } else { m_restoreActive = false; } } if (!suppressed) emit viewportAdjustNeeded(h - oldH); } } if (!m_relayoutPending) { m_relayoutPending = true; QTimer::singleShot(0, this, [this] { m_relayoutPending = false; relayout(); polish(); }); } } }; connect( entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); auto* attached = qobject_cast( qmlAttachedPropertiesObject(entry.item, false)); if (attached) { connect( attached, &ChatListViewAttached::preferredHeightChanged, this, onHeightChanged); connect( attached, &ChatListViewAttached::visibleHeightChanged, this, [this] { polish(); }); connect( attached, &ChatListViewAttached::readyChanged, this, [this, item = entry.item] { auto indexIt = m_itemToIndex.find(item); if (indexIt == m_itemToIndex.end()) return; const int idx = indexIt.value(); if (idx >= static_cast(m_layout.size())) return; auto* att = qobject_cast( qmlAttachedPropertiesObject(item, false)); if (!att || !att->ready()) return; const qreal h = delegateHeight(item); const qreal oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight(); const bool wasKnown = m_layout[idx].heightKnown; if (wasKnown) untrackHeight(m_layout[idx].height); m_layout[idx].height = h; m_layout[idx].heightKnown = true; m_layout[idx].measuredWidth = width(); trackHeight(h); auto delegateIt = m_delegates.find(idx); if (delegateIt != m_delegates.end() && delegateIt->measureOnly) delegateIt->lastHeightChange = m_clock.elapsed(); if (!wasKnown) { m_animateY = true; } if (att->trackViewport() && !m_anchorItem && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0) && m_layout[idx].targetY < m_viewport.y()) { emit viewportAdjustNeeded(h - oldLayoutH); } polish(); }); } return entry; } void ChatListView::destroyDelegate(DelegateEntry& entry) { if (entry.item) { entry.item->setParentItem(nullptr); entry.item->setVisible(false); entry.item->deleteLater(); entry.item = nullptr; } } void ChatListView::updateDelegateData(DelegateEntry& entry) { if (!m_model || !entry.item) return; const auto roleNames = m_model->roleNames(); const auto index = m_model->index(entry.modelIndex, 0); bool hasModelData = false; for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { const auto name = QString::fromUtf8(it.value()); entry.item->setProperty( name.toUtf8().constData(), m_model->data(index, it.key())); if (name == QStringLiteral("modelData")) hasModelData = true; } entry.item->setProperty("index", entry.modelIndex); if (!hasModelData) { const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); entry.item->setProperty("modelData", m_model->data(index, role)); } } void ChatListView::connectModel() { if (!m_model) return; m_modelConnections = { connect( m_model, &QAbstractItemModel::rowsInserted, this, &ChatListView::onRowsInserted), connect( m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ChatListView::onRowsAboutToBeRemoved), connect( m_model, &QAbstractItemModel::rowsRemoved, this, &ChatListView::onRowsRemoved), connect( m_model, &QAbstractItemModel::dataChanged, this, &ChatListView::onDataChanged), connect( m_model, &QAbstractItemModel::modelReset, this, &ChatListView::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; resetContent(); emit modelChanged(); }), }; } void ChatListView::disconnectModel() { for (auto& conn : m_modelConnections) disconnect(conn); m_modelConnections.clear(); } void ChatListView::resetContent() { for (auto& entry : m_delegates) destroyDelegate(entry); m_delegates.clear(); m_itemToIndex.clear(); for (auto& entry : m_dyingDelegates) destroyDelegate(entry); m_dyingDelegates.clear(); m_knownHeightSum = 0; m_knownHeightCount = 0; m_layout.clear(); m_measureQueue.clear(); if (m_model && m_componentComplete) { const int rows = m_model->rowCount(); m_layout.resize(rows); for (int i = 0; i < rows; ++i) { m_layout[i].height = 0; m_layout[i].heightKnown = false; m_layout[i].measuredWidth = -1; } emit countChanged(); } polish(); } void ChatListView::onRowsInserted( const QModelIndex& parent, int first, int last) { if (parent.isValid()) return; const int insertCount = last - first + 1; m_layout.insert(first, insertCount, ItemRecord{0, 0, false, true}); for (int& i : m_measureQueue) if (i >= first) i += insertCount; QHash shifted; for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { int newIdx = it.key() >= first ? it.key() + insertCount : it.key(); auto entry = std::move(it.value()); entry.modelIndex = newIdx; if (entry.item) { entry.item->setProperty("index", newIdx); m_itemToIndex[entry.item] = newIdx; } shifted.insert(newIdx, std::move(entry)); } m_delegates = std::move(shifted); emit countChanged(); m_animateY = true; polish(); } void ChatListView::onRowsAboutToBeRemoved( const QModelIndex& parent, int first, int last) { if (parent.isValid()) return; for (int i = first; i <= last; ++i) { if (!m_delegates.contains(i)) continue; auto entry = m_delegates.take(i); if (entry.item) m_itemToIndex.remove(entry.item); entry.pendingRemoval = true; if (entry.pendingInsert) { destroyDelegate(entry); continue; } if (m_removeDuration > 0 && entry.item) { auto* attached = qobject_cast( qmlAttachedPropertiesObject(entry.item, false)); if (attached) attached->setRemoving(true); auto* item = entry.item; QTimer::singleShot(m_removeDuration, this, [this, item] { for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) { if (it->item == item) { destroyDelegate(*it); m_dyingDelegates.erase(it); return; } } }); m_dyingDelegates.append(std::move(entry)); } else { destroyDelegate(entry); } } } void ChatListView::onRowsRemoved( const QModelIndex& parent, int first, int last) { if (parent.isValid()) return; const int removeCount = last - first + 1; for (int i = first; i <= last; ++i) { if (m_layout[i].heightKnown) untrackHeight(m_layout[i].height); } QVector queue; queue.reserve(m_measureQueue.size()); for (int i : m_measureQueue) { if (i >= first && i <= last) continue; if (i > last) i -= removeCount; queue.append(i); } m_measureQueue = std::move(queue); m_layout.remove(first, removeCount); QHash shifted; for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { int newIdx = it.key() > last ? it.key() - removeCount : it.key(); auto entry = std::move(it.value()); entry.modelIndex = newIdx; if (entry.item) { entry.item->setProperty("index", newIdx); m_itemToIndex[entry.item] = newIdx; } shifted.insert(newIdx, std::move(entry)); } m_delegates = std::move(shifted); emit countChanged(); m_animateY = true; polish(); } void ChatListView::onDataChanged( const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) { Q_UNUSED(roles) if (topLeft.parent().isValid()) return; for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { if (m_delegates.contains(i)) updateDelegateData(m_delegates[i]); } } void ChatListView::onModelReset() { if (!m_model) { resetContent(); return; } const int newRows = m_model->rowCount(); const int oldRows = static_cast(m_layout.size()); 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) { for (auto& entry : m_delegates) updateDelegateData(entry); return; } } resetContent(); if (!qFuzzyIsNull(m_viewport.y())) { emit viewportAdjustNeeded(-m_viewport.y()); } } qreal ChatListView::effectiveEstimatedHeight() const { if (m_estimatedHeight >= 0) return m_estimatedHeight; if (m_knownHeightCount > 0) return m_knownHeightSum / m_knownHeightCount; return 40; } void ChatListView::trackHeight(qreal height) { m_knownHeightSum += height; ++m_knownHeightCount; } void ChatListView::untrackHeight(qreal height) { m_knownHeightSum -= height; --m_knownHeightCount; } qreal ChatListView::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(); } qreal ChatListView::delegateVisibleHeight(QQuickItem* item) { if (!item) return 0; auto* attached = qobject_cast( qmlAttachedPropertiesObject(item, false)); if (attached) { if (attached->visibleHeight() >= 0) return attached->visibleHeight(); if (attached->preferredHeight() >= 0) return attached->preferredHeight(); } return item->implicitHeight(); } bool ChatListView::isDelegateReady(QQuickItem* item) { if (!item) return false; auto* att = qobject_cast( qmlAttachedPropertiesObject(item, false)); return !att || att->ready(); } void ChatListView::anchorItem(QQuickItem* item, Edge edge) { if (!item) return; if (m_anchorItem == item) { if (m_anchorEdge != edge) { m_anchorEdge = edge; applyAnchor(); } return; } m_anchorItem = item; m_anchorEdge = edge; connect(item, &QObject::destroyed, this, [this, item] { if (m_anchorItem == item) clearAnchor(); }); applyAnchor(); } void ChatListView::clearAnchor() { if (!m_anchorItem) return; for (auto* it = m_anchorItem.data(); it; it = it->parentItem()) { auto delegateIt = m_itemToIndex.find(it); if (delegateIt != m_itemToIndex.end()) { m_restoreIndex = delegateIt.value(); break; } } m_anchorItem = nullptr; } void ChatListView::requestRestore(qreal targetY) { m_restoreActive = true; m_restoreQuietUntil = m_clock.elapsed() + RESTORE_QUIET_MS; const qreal target = qBound(0.0, targetY, qMax(0.0, m_layoutHeight - m_viewport.height())); if (!qFuzzyCompare(target + 1.0, m_viewport.y() + 1.0)) { emit viewportRestoreRequested(target); } } void ChatListView::applyAnchor() { const auto item = m_anchorItem; if (!item || m_viewport.isNull()) return; const qreal a = item->mapToItem(this, 0, 0).y(); const qreal b = item->mapToItem(this, 0, item->height()).y(); const qreal near = std::min(a, b); const qreal far = std::max(a, b); const qreal vpTop = m_viewport.y(); const qreal vpBottom = m_viewport.y() + m_viewport.height(); qreal next = -1; if (m_anchorEdge == TopEdge) { if (far > vpBottom) next = far - m_viewport.height(); } else { if (near < vpTop) next = near; } if (next < 0) return; next = qBound(0.0, next, qMax(0.0, m_layoutHeight - m_viewport.height())); if (!qFuzzyCompare(next + 1.0, vpTop + 1.0)) { emit viewportAdjustNeeded(next - vpTop); } } } // namespace ZShell::components