Files
z-bar-qt/Plugins/ZShell/Components/chatlistview.cpp
T
zach 08d42e1f8a
C++ / fmt (pull_request) Successful in 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 45s
chore: formatter & clangd lsp option
2026-10-18 15:23:40 +02:00

1236 lines
31 KiB
C++

#include "chatlistview.hpp"
#include <QEvent>
#include <QDebug>
#include <QQmlContext>
#include <QTimer>
#include <algorithm>
#include <cmath>
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<int> 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<int>(m_layout.size())) {
qreal visualY = 0;
bool hasVisItem = false;
for (int i = 0; i < static_cast<int>(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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<int>(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<int>(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<int, int> 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<int>(m_layout.size()) - 1;
int first = static_cast<int>(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<int>(m_layout.size())) return {-1, -1};
int last = first;
for (int i = first; i < static_cast<int>(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<int> visibleIndices;
if (first >= 0) {
for (int i = first; i <= last; ++i)
visibleIndices.insert(i);
}
const auto vp = effectiveViewport();
QList<int> 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<int>(toRemove.size());
QVector<DelegateEntry> removedEntries;
removedEntries.reserve(
std::min(destroyBudget, static_cast<int>(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<int> 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<int>(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<int>(toRemove.size()) ||
created < static_cast<int>(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<int>(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<int> 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<int>(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<QQuickItem*>(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<int>(m_layout.size()) &&
m_layout[modelIndex].isNew) {
auto* addingAttached = qobject_cast<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<int>(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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<int>(m_layout.size())) return;
auto* att = qobject_cast<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<int, DelegateEntry> 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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<int> 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<int, DelegateEntry> 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<int>& 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<int>(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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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<ChatListViewAttached*>(
qmlAttachedPropertiesObject<ChatListView>(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