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
1236 lines
31 KiB
C++
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
|