C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
807 lines
22 KiB
C++
807 lines
22 KiB
C++
#include "snapview.hpp"
|
|
|
|
#include <QQuickWindow>
|
|
#include <QTimer>
|
|
#include <private/qquickflickable_p.h>
|
|
#include <private/qquickflickable_p_p.h>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
|
|
namespace ZShell::components {
|
|
namespace {
|
|
|
|
// The same device-pixel helpers SnapLayout uses: the residual is the fractional
|
|
// part of an absolute device-pixel position, which is how a sub-pixel move of
|
|
// the view or its ancestors is noticed without reacting to whole-pixel moves.
|
|
qint64 originResidual(qreal logical, qreal dpr) {
|
|
const qreal device = logical * dpr;
|
|
return std::llround((device - std::floor(device)) * 1024.0) % 1024;
|
|
}
|
|
|
|
QPointF absoluteOrigin(const QQuickItem* item) {
|
|
qreal x = 0.0;
|
|
qreal y = 0.0;
|
|
for (const QQuickItem* it = item; it; it = it->parentItem()) {
|
|
x += it->x();
|
|
y += it->y();
|
|
}
|
|
return QPointF(x, y);
|
|
}
|
|
|
|
constexpr int AsyncBatchSize = 4;
|
|
|
|
// Sizes are compared with a tolerance rather than ==, because the snapped
|
|
// logical values are quotients of device pixels and never exact.
|
|
bool differs(qreal a, qreal b) {
|
|
return qAbs(a - b) > 0.001;
|
|
}
|
|
|
|
// Geometry is watched to notice sub-pixel moves; Destroyed is watched so a
|
|
// listener can be dropped while the item is still valid instead of at teardown.
|
|
constexpr QFlags<QQuickItemPrivate::ChangeType> WatchedChanges =
|
|
QQuickItemPrivate::Geometry | QQuickItemPrivate::Destroyed;
|
|
|
|
void addGeometryListener(QQuickItem* item, QQuickItemChangeListener* listener) {
|
|
if (item)
|
|
QQuickItemPrivate::get(item)->addItemChangeListener(
|
|
listener, WatchedChanges);
|
|
}
|
|
|
|
void removeGeometryListener(
|
|
QQuickItem* item, QQuickItemChangeListener* listener) {
|
|
if (item)
|
|
QQuickItemPrivate::get(item)->removeItemChangeListener(
|
|
listener, WatchedChanges);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
SnapView::SnapView(QQuickItem* parent) : QQuickFlickable(parent) {
|
|
setClip(true);
|
|
|
|
// Asynchronous batches are spread over event loop turns, so the frames
|
|
// between them can render; polish() from updatePolish would run them all
|
|
// inside one polish flush.
|
|
m_asyncTimer = new QTimer(this);
|
|
m_asyncTimer->setSingleShot(true);
|
|
m_asyncTimer->setInterval(0);
|
|
connect(m_asyncTimer, &QTimer::timeout, this, &SnapView::polish);
|
|
}
|
|
|
|
SnapView::~SnapView() {
|
|
clearListeners();
|
|
disconnectModel();
|
|
}
|
|
|
|
QAbstractItemModel* SnapView::model() const {
|
|
return m_model.data();
|
|
}
|
|
|
|
void SnapView::setModel(QAbstractItemModel* model) {
|
|
if (m_model == model) return;
|
|
|
|
disconnectModel();
|
|
for (int index : m_items.keys())
|
|
destroyDelegate(index);
|
|
m_sizeDevCache.clear();
|
|
m_rowSizes.clear();
|
|
|
|
m_model = model;
|
|
invalidateRoleNames();
|
|
connectModel();
|
|
rebuildRows();
|
|
|
|
if (m_componentComplete) polish();
|
|
emit modelChanged();
|
|
}
|
|
|
|
QQmlComponent* SnapView::delegate() const {
|
|
return m_delegate.data();
|
|
}
|
|
|
|
void SnapView::setDelegate(QQmlComponent* delegate) {
|
|
if (m_delegate == delegate) return;
|
|
|
|
m_delegate = delegate;
|
|
for (int index : m_items.keys())
|
|
destroyDelegate(index);
|
|
m_pendingCreate.clear();
|
|
m_sizeDevCache.clear();
|
|
|
|
if (m_componentComplete) polish();
|
|
emit delegateChanged();
|
|
}
|
|
|
|
Qt::Orientation SnapView::orientation() const {
|
|
return m_orientation;
|
|
}
|
|
|
|
void SnapView::setOrientation(Qt::Orientation orientation) {
|
|
if (m_orientation == orientation) return;
|
|
m_orientation = orientation;
|
|
invalidateMainAxisCaches();
|
|
if (m_componentComplete) polish();
|
|
emit orientationChanged();
|
|
}
|
|
|
|
qreal SnapView::spacing() const {
|
|
return m_spacing;
|
|
}
|
|
|
|
void SnapView::setSpacing(qreal spacing) {
|
|
if (qFuzzyCompare(m_spacing, spacing)) return;
|
|
m_spacing = spacing;
|
|
if (m_componentComplete) polish();
|
|
emit spacingChanged();
|
|
}
|
|
|
|
qreal SnapView::cacheBuffer() const {
|
|
return m_cacheBuffer;
|
|
}
|
|
|
|
void SnapView::setCacheBuffer(qreal buffer) {
|
|
if (qFuzzyCompare(m_cacheBuffer, buffer)) return;
|
|
m_cacheBuffer = buffer;
|
|
if (m_componentComplete) polish();
|
|
emit cacheBufferChanged();
|
|
}
|
|
|
|
qreal SnapView::estimatedSize() const {
|
|
return m_estimatedSize;
|
|
}
|
|
|
|
void SnapView::setEstimatedSize(qreal size) {
|
|
if (qFuzzyCompare(m_estimatedSize, size)) return;
|
|
m_estimatedSize = size;
|
|
if (m_componentComplete) polish();
|
|
emit estimatedSizeChanged();
|
|
}
|
|
|
|
bool SnapView::snapSizes() const {
|
|
return m_snapSizes;
|
|
}
|
|
|
|
void SnapView::setSnapSizes(bool snapSizes) {
|
|
if (m_snapSizes == snapSizes) return;
|
|
m_snapSizes = snapSizes;
|
|
if (m_componentComplete) polish();
|
|
emit snapSizesChanged();
|
|
}
|
|
|
|
bool SnapView::asynchronous() const {
|
|
return m_asynchronous;
|
|
}
|
|
|
|
void SnapView::setAsynchronous(bool asynchronous) {
|
|
if (m_asynchronous == asynchronous) return;
|
|
m_asynchronous = asynchronous;
|
|
if (m_componentComplete) polish();
|
|
emit asynchronousChanged();
|
|
}
|
|
|
|
int SnapView::count() const {
|
|
return m_model ? m_model->rowCount() : 0;
|
|
}
|
|
|
|
int SnapView::currentIndex() const {
|
|
return m_currentIndex;
|
|
}
|
|
|
|
void SnapView::setCurrentIndex(int index) {
|
|
if (m_currentIndex == index) return;
|
|
m_currentIndex = index;
|
|
emit currentIndexChanged();
|
|
emit currentItemChanged();
|
|
}
|
|
|
|
QQuickItem* SnapView::currentItem() const {
|
|
return m_items.value(m_currentIndex, nullptr);
|
|
}
|
|
|
|
void SnapView::positionViewAtBeginning() {
|
|
positionViewAtIndex(0);
|
|
}
|
|
|
|
void SnapView::positionViewAtEnd() {
|
|
positionViewAtIndex(count() - 1);
|
|
}
|
|
|
|
void SnapView::positionViewAtIndex(int index) {
|
|
if (!m_componentComplete || index < 0 || index >= m_rows.size()) return;
|
|
|
|
// Summed here rather than read from m_rows, whose offsets only refresh on
|
|
// polish: positioning must be right even when called between passes.
|
|
const bool horiz = m_orientation == Qt::Horizontal;
|
|
const qreal extent = horiz ? width() : height();
|
|
const qreal spacing = effectiveSpacing();
|
|
|
|
qreal offset = 0.0;
|
|
qreal target = 0.0;
|
|
qreal total = 0.0;
|
|
for (int i = 0; i < m_rows.size(); ++i) {
|
|
if (i == index) target = offset;
|
|
const qreal size = rowSize(i);
|
|
total = offset + size;
|
|
offset += size + spacing;
|
|
}
|
|
|
|
const qreal position =
|
|
std::clamp(target, 0.0, (std::max)(0.0, total - extent));
|
|
const qint64 positionDev = std::llround(position * devicePixelRatio());
|
|
const qreal snapped = qreal(positionDev) / devicePixelRatio();
|
|
if (horiz)
|
|
setContentX(snapped);
|
|
else
|
|
setContentY(snapped);
|
|
}
|
|
|
|
qreal SnapView::devicePixelRatio() const {
|
|
const QQuickWindow* win = window();
|
|
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
|
|
return dpr > 0.0 ? dpr : 1.0;
|
|
}
|
|
|
|
void SnapView::componentComplete() {
|
|
QQuickFlickable::componentComplete();
|
|
m_componentComplete = true;
|
|
rebuildRows();
|
|
updateAncestorListeners();
|
|
polish();
|
|
}
|
|
|
|
void SnapView::geometryChange(
|
|
const QRectF& newGeometry, const QRectF& oldGeometry) {
|
|
QQuickFlickable::geometryChange(newGeometry, oldGeometry);
|
|
if (m_componentComplete && !m_placing &&
|
|
newGeometry.size() != oldGeometry.size()) {
|
|
polish();
|
|
}
|
|
}
|
|
|
|
void SnapView::itemChange(ItemChange change, const ItemChangeData& value) {
|
|
QQuickFlickable::itemChange(change, value);
|
|
if (change == ItemSceneChange) {
|
|
// Leaving the scene means the ancestors are going away: drop the
|
|
// listeners now, while both sides are still whole.
|
|
if (value.window) {
|
|
updateAncestorListeners();
|
|
polish();
|
|
} else {
|
|
clearListeners();
|
|
}
|
|
return;
|
|
}
|
|
if (change == ItemDevicePixelRatioHasChanged) {
|
|
updateAncestorListeners();
|
|
polish();
|
|
}
|
|
}
|
|
|
|
void SnapView::updatePolish() {
|
|
if (!m_componentComplete) return;
|
|
layoutAndPlace();
|
|
if (!m_pendingCreate.isEmpty() && m_asynchronous) m_asyncTimer->start();
|
|
}
|
|
|
|
void SnapView::itemGeometryChanged(
|
|
QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) {
|
|
Q_UNUSED(oldGeometry)
|
|
if (!m_componentComplete || m_placing) return;
|
|
|
|
const qreal dpr = devicePixelRatio();
|
|
|
|
QQuickItem* content = QQuickFlickablePrivate::get(this)->contentItem;
|
|
const bool watched = std::any_of(
|
|
m_ancestorListeners.constBegin(),
|
|
m_ancestorListeners.constEnd(),
|
|
[item](const QPointer<QQuickItem>& p) { return p == item; });
|
|
if (item == content || watched) {
|
|
const QPointF origin = contentOrigin();
|
|
if (originResidual(origin.x(), dpr) != m_originResidualX ||
|
|
originResidual(origin.y(), dpr) != m_originResidualY) {
|
|
polish();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (change.sizeChange() && m_items.key(item, -1) >= 0) polish();
|
|
}
|
|
|
|
void SnapView::itemDestroyed(QQuickItem* item) {
|
|
m_sizeDevCache.remove(item);
|
|
const auto isItem = [item](const QPointer<QQuickItem>& p) {
|
|
return p == item;
|
|
};
|
|
m_delegateListeners.erase(
|
|
std::remove_if(
|
|
m_delegateListeners.begin(), m_delegateListeners.end(), isItem),
|
|
m_delegateListeners.end());
|
|
m_ancestorListeners.erase(
|
|
std::remove_if(
|
|
m_ancestorListeners.begin(), m_ancestorListeners.end(), isItem),
|
|
m_ancestorListeners.end());
|
|
const int index = m_items.key(item, -1);
|
|
if (index >= 0) {
|
|
m_items.remove(index);
|
|
emit currentItemChanged();
|
|
}
|
|
}
|
|
|
|
qreal SnapView::effectiveSpacing() const {
|
|
return qreal(std::llround(m_spacing * devicePixelRatio())) /
|
|
devicePixelRatio();
|
|
}
|
|
|
|
qreal SnapView::rowSize(int index) const {
|
|
QQuickItem* item = m_items.value(index, nullptr);
|
|
if (item) {
|
|
const qreal measured = m_orientation == Qt::Horizontal ? item->width()
|
|
: item->height();
|
|
if (measured > 0.0) return measured;
|
|
}
|
|
|
|
const qreal cached = m_rowSizes.value(index, 0.0);
|
|
if (cached > 0.0) return cached;
|
|
return m_estimatedSize > 0.0 ? m_estimatedSize : 0.0;
|
|
}
|
|
|
|
qint64 SnapView::snappedDeviceMainSize(QQuickItem* item) {
|
|
const qreal dpr = devicePixelRatio();
|
|
|
|
const qreal own = m_orientation == Qt::Horizontal ? item->width()
|
|
: item->height();
|
|
if (own <= 0.0) return 0;
|
|
|
|
// Reuse the cached device size while the item's own size is still within a
|
|
// device pixel of it, so an item that only moves keeps its size.
|
|
const qint64 cached = m_sizeDevCache.value(item, -1);
|
|
if (cached >= 0 && qAbs(own * dpr - qreal(cached)) <= 1.0) return cached;
|
|
|
|
const qint64 snapped = std::llround(own * dpr);
|
|
m_sizeDevCache[item] = snapped;
|
|
return snapped;
|
|
}
|
|
|
|
int SnapView::firstVisibleIndex() const {
|
|
if (m_rows.isEmpty()) return 0;
|
|
|
|
// Row ends are non-decreasing (offsets accumulate size plus spacing), so the
|
|
// visible range can be found by binary search instead of a linear scan.
|
|
const qreal start = m_orientation == Qt::Horizontal ? contentX()
|
|
: contentY();
|
|
const qreal from = (std::max)(0.0, start - m_cacheBuffer);
|
|
|
|
const auto it = std::lower_bound(
|
|
m_rows.constBegin(),
|
|
m_rows.constEnd(),
|
|
from,
|
|
[](const Row& row, qreal value) {
|
|
return row.exactOffset + row.exactSize < value;
|
|
});
|
|
return static_cast<int>(std::distance(m_rows.constBegin(), it));
|
|
}
|
|
|
|
int SnapView::lastVisibleIndex() const {
|
|
if (m_rows.isEmpty()) return 0;
|
|
|
|
const bool horiz = m_orientation == Qt::Horizontal;
|
|
const qreal start = horiz ? contentX() : contentY();
|
|
const qreal extent = horiz ? width() : height();
|
|
const qreal to = start + extent + m_cacheBuffer;
|
|
|
|
const auto it = std::lower_bound(
|
|
m_rows.constBegin(),
|
|
m_rows.constEnd(),
|
|
to,
|
|
[](const Row& row, qreal value) { return row.exactOffset <= value; });
|
|
if (it == m_rows.constBegin()) return 0;
|
|
return static_cast<int>(std::distance(m_rows.constBegin(), it)) - 1;
|
|
}
|
|
|
|
QPointF SnapView::contentOrigin() const {
|
|
// QQuickFlickablePrivate::get has no const overload; the item private is the
|
|
// base of the flickable private, so casting is the const path to it.
|
|
const auto* priv = static_cast<const QQuickFlickablePrivate*>(
|
|
QQuickItemPrivate::get(this));
|
|
return absoluteOrigin(priv->contentItem);
|
|
}
|
|
|
|
void SnapView::layoutAndPlace() {
|
|
const qreal dpr = devicePixelRatio();
|
|
|
|
const bool horiz = m_orientation == Qt::Horizontal;
|
|
const qreal spacing = effectiveSpacing();
|
|
|
|
// Cumulative exact offsets, so the rounding below has the same exact
|
|
// reference SnapLayout's arrangeChildren works from. Running this before
|
|
// updateDelegates gives the viewport range to recycle against, and running
|
|
// it again afterwards picks up sizes that changed when a re-keyed delegate
|
|
// got a new index.
|
|
const auto accumulate = [&]() {
|
|
qreal offset = 0.0;
|
|
for (int i = 0; i < m_rows.size(); ++i) {
|
|
qreal size = rowSize(i);
|
|
QQuickItem* item = m_items.value(i, nullptr);
|
|
if (item) {
|
|
const qreal measured = horiz ? item->width() : item->height();
|
|
if (measured > 0.0) m_rowSizes[i] = measured;
|
|
}
|
|
|
|
m_rows[i].exactOffset = offset;
|
|
m_rows[i].exactSize = size;
|
|
offset += size + spacing;
|
|
}
|
|
return m_rows.isEmpty() ? 0.0 : offset - spacing;
|
|
};
|
|
|
|
accumulate();
|
|
updateDelegates();
|
|
const qreal total = accumulate();
|
|
|
|
if (horiz)
|
|
setContentWidth(qreal(std::llround(total * dpr)) / dpr);
|
|
else
|
|
setContentHeight(qreal(std::llround(total * dpr)) / dpr);
|
|
|
|
QQuickItem* content = QQuickFlickablePrivate::get(this)->contentItem;
|
|
if (!content) return;
|
|
|
|
const QPointF origin = absoluteOrigin(content);
|
|
const qint64 originXDev = std::llround(origin.x() * dpr);
|
|
const qint64 originYDev = std::llround(origin.y() * dpr);
|
|
const qint64 crossDev = std::llround((horiz ? height() : width()) * dpr);
|
|
|
|
qint64 prevNear = 0;
|
|
qint64 prevSizeDev = 0;
|
|
qreal prevEndExact = 0.0;
|
|
bool started = false;
|
|
|
|
m_placing = true;
|
|
for (int i = 0; i < m_rows.size(); ++i) {
|
|
const Row& row = m_rows.at(i);
|
|
const qreal mainExact = row.exactOffset * dpr;
|
|
|
|
qint64 mainNear = 0;
|
|
if (!started) {
|
|
mainNear = std::llround(mainExact);
|
|
started = true;
|
|
} else {
|
|
mainNear =
|
|
prevNear + prevSizeDev + std::llround(mainExact - prevEndExact);
|
|
}
|
|
|
|
QQuickItem* item = m_items.value(i, nullptr);
|
|
qint64 mainDev = std::llround(row.exactSize * dpr);
|
|
if (item) {
|
|
const qint64 ownDev = snappedDeviceMainSize(item);
|
|
if (ownDev > 0) mainDev = ownDev;
|
|
}
|
|
|
|
prevEndExact = mainExact + row.exactSize * dpr;
|
|
prevSizeDev = mainDev;
|
|
prevNear = mainNear;
|
|
|
|
if (!item) continue;
|
|
|
|
const qreal mainSize = qreal(mainDev) / dpr;
|
|
const qreal crossSize = qreal(crossDev) / dpr;
|
|
|
|
const qreal posX = horiz
|
|
? qreal(originXDev + mainNear) / dpr - origin.x()
|
|
: qreal(originXDev) / dpr - origin.x();
|
|
const qreal posY =
|
|
horiz ? qreal(originYDev) / dpr - origin.y()
|
|
: qreal(originYDev + mainNear) / dpr - origin.y();
|
|
item->setPosition(QPointF(posX, posY));
|
|
|
|
// Only the cross axis is written unconditionally. Writing the main axis
|
|
// would replace the delegate's implicit-size fallback with an explicit
|
|
// value, so a delegate whose content changes size could never grow
|
|
// again. It is resized only when snapSizes asks for exactly that.
|
|
if (horiz) {
|
|
if (m_snapSizes && mainDev > 0 && differs(item->width(), mainSize))
|
|
item->setWidth(mainSize);
|
|
if (differs(item->height(), crossSize)) item->setHeight(crossSize);
|
|
} else {
|
|
if (differs(item->width(), crossSize)) item->setWidth(crossSize);
|
|
if (m_snapSizes && mainDev > 0 && differs(item->height(), mainSize))
|
|
item->setHeight(mainSize);
|
|
}
|
|
}
|
|
m_placing = false;
|
|
|
|
trackOriginResidual();
|
|
}
|
|
|
|
void SnapView::updateDelegates() {
|
|
const int first = firstVisibleIndex();
|
|
const int last = lastVisibleIndex();
|
|
|
|
for (int index : m_items.keys()) {
|
|
if (index < first || index > last) destroyDelegate(index);
|
|
}
|
|
|
|
QVector<int> missing;
|
|
for (int index = first; index <= last; ++index) {
|
|
if (m_items.contains(index))
|
|
updateDelegateData(index);
|
|
else
|
|
missing.append(index);
|
|
}
|
|
|
|
int budget = m_asynchronous ? AsyncBatchSize
|
|
: (std::numeric_limits<int>::max)();
|
|
m_pendingCreate.clear();
|
|
for (int index : std::as_const(missing)) {
|
|
if (budget <= 0) {
|
|
m_pendingCreate.append(index);
|
|
continue;
|
|
}
|
|
(void)createDelegate(index);
|
|
--budget;
|
|
}
|
|
}
|
|
|
|
QQuickItem* SnapView::createDelegate(int index) {
|
|
if (!m_delegate || !m_model || index < 0 || index >= m_model->rowCount())
|
|
return nullptr;
|
|
if (m_items.contains(index)) return m_items.value(index);
|
|
|
|
// Same as LazyListView: beginCreate + setInitialProperties + completeCreate,
|
|
// so delegates with pragma ComponentBehavior: Bound are accepted.
|
|
auto* compContext = m_delegate->creationContext();
|
|
if (!compContext) compContext = qmlContext(this);
|
|
if (!compContext) return nullptr;
|
|
|
|
QObject* obj = m_delegate->beginCreate(compContext);
|
|
QQuickItem* item = qobject_cast<QQuickItem*>(obj);
|
|
if (!item) {
|
|
qWarning(
|
|
"SnapView: delegate creation failed for row %d: %s",
|
|
index,
|
|
qPrintable(m_delegate->errorString()));
|
|
if (obj) m_delegate->completeCreate();
|
|
delete obj;
|
|
return nullptr;
|
|
}
|
|
|
|
m_delegate->setInitialProperties(item, delegateProperties(index));
|
|
|
|
QQuickItem* content = QQuickFlickablePrivate::get(this)->contentItem;
|
|
item->setParentItem(content ? content : this);
|
|
addGeometryListener(item, this);
|
|
m_delegateListeners.append(item);
|
|
m_items.insert(index, item);
|
|
|
|
m_delegate->completeCreate();
|
|
|
|
emit currentItemChanged();
|
|
return item;
|
|
}
|
|
|
|
void SnapView::destroyDelegate(int index) {
|
|
QQuickItem* item = m_items.take(index);
|
|
if (!item) return;
|
|
|
|
removeGeometryListener(item, this);
|
|
m_delegateListeners.removeAll(item);
|
|
m_sizeDevCache.remove(item);
|
|
item->deleteLater();
|
|
emit currentItemChanged();
|
|
}
|
|
|
|
void SnapView::updateDelegateData(int index) {
|
|
QQuickItem* item = m_items.value(index, nullptr);
|
|
if (!item) return;
|
|
|
|
const QVariantMap props = delegateProperties(index);
|
|
for (auto it = props.constBegin(); it != props.constEnd(); ++it) {
|
|
const QByteArray name = it.key().toUtf8();
|
|
// Scrolls refresh every visible delegate each pass; writing only the
|
|
// values that actually changed keeps that cheap.
|
|
if (item->property(name.constData()) == it.value()) continue;
|
|
item->setProperty(name.constData(), it.value());
|
|
}
|
|
}
|
|
|
|
QVariantMap SnapView::delegateProperties(int index) {
|
|
QVariantMap props;
|
|
if (!m_model || index < 0 || index >= m_model->rowCount()) return props;
|
|
|
|
const QModelIndex srcIndex = m_model->index(index, 0);
|
|
const auto& names = roleNames();
|
|
bool hasModelData = false;
|
|
|
|
for (auto it = names.constBegin(); it != names.constEnd(); ++it) {
|
|
const auto name = QString::fromUtf8(it.value());
|
|
props.insert(name, m_model->data(srcIndex, it.key()));
|
|
if (name == QStringLiteral("modelData")) hasModelData = true;
|
|
}
|
|
|
|
props.insert(QStringLiteral("index"), index);
|
|
|
|
if (!hasModelData) {
|
|
const auto role = names.isEmpty() ? Qt::DisplayRole
|
|
: names.constBegin().key();
|
|
props.insert(QStringLiteral("modelData"), m_model->data(srcIndex, role));
|
|
}
|
|
|
|
return props;
|
|
}
|
|
|
|
const QHash<int, QByteArray>& SnapView::roleNames() {
|
|
if (!m_roleNamesValid) {
|
|
m_roleNames = m_model ? m_model->roleNames() : QHash<int, QByteArray>();
|
|
m_roleNamesValid = true;
|
|
}
|
|
return m_roleNames;
|
|
}
|
|
|
|
void SnapView::invalidateRoleNames() {
|
|
m_roleNamesValid = false;
|
|
}
|
|
|
|
void SnapView::invalidateMainAxisCaches() {
|
|
// Cached sizes belong to the axis that is no longer the main one.
|
|
m_rowSizes.fill(0.0);
|
|
m_sizeDevCache.clear();
|
|
}
|
|
|
|
void SnapView::rebuildRows() {
|
|
m_rows.resize(count());
|
|
m_rowSizes.resize(count());
|
|
if (m_currentIndex >= count()) m_currentIndex = -1;
|
|
emit countChanged();
|
|
emit currentItemChanged();
|
|
}
|
|
|
|
void SnapView::trackOriginResidual() {
|
|
const qreal dpr = devicePixelRatio();
|
|
const QPointF origin = contentOrigin();
|
|
m_originResidualX = originResidual(origin.x(), dpr);
|
|
m_originResidualY = originResidual(origin.y(), dpr);
|
|
}
|
|
|
|
void SnapView::shiftDelegateIndices(int from, int delta) {
|
|
if (delta == 0) return;
|
|
|
|
QHash<int, QQuickItem*> shifted;
|
|
for (auto it = m_items.constBegin(); it != m_items.constEnd(); ++it) {
|
|
shifted.insert(
|
|
it.key() >= from ? it.key() + delta : it.key(), it.value());
|
|
}
|
|
m_items = shifted;
|
|
|
|
for (int& index : m_pendingCreate) {
|
|
if (index >= from) index += delta;
|
|
}
|
|
}
|
|
|
|
void SnapView::updateAncestorListeners() {
|
|
QQuickItem* content = QQuickFlickablePrivate::get(this)->contentItem;
|
|
QVector<QPointer<QQuickItem>> want;
|
|
if (content) want.append(content);
|
|
for (QQuickItem* it = parentItem(); it; it = it->parentItem())
|
|
want.append(it);
|
|
|
|
for (const QPointer<QQuickItem>& item :
|
|
std::as_const(m_ancestorListeners)) {
|
|
if (item && !want.contains(item))
|
|
removeGeometryListener(item.data(), this);
|
|
}
|
|
for (const QPointer<QQuickItem>& item : std::as_const(want)) {
|
|
const bool known = std::any_of(
|
|
m_ancestorListeners.constBegin(),
|
|
m_ancestorListeners.constEnd(),
|
|
[&item](const QPointer<QQuickItem>& p) { return p == item; });
|
|
if (!known) addGeometryListener(item.data(), this);
|
|
}
|
|
m_ancestorListeners = want;
|
|
|
|
trackOriginResidual();
|
|
}
|
|
|
|
void SnapView::clearListeners() {
|
|
for (const QPointer<QQuickItem>& item : std::as_const(m_ancestorListeners))
|
|
removeGeometryListener(item.data(), this);
|
|
m_ancestorListeners.clear();
|
|
m_delegateListeners.clear();
|
|
}
|
|
|
|
void SnapView::connectModel() {
|
|
if (!m_model) return;
|
|
m_modelConnections.append(
|
|
QObject::connect(
|
|
m_model,
|
|
&QAbstractItemModel::rowsInserted,
|
|
this,
|
|
&SnapView::onRowsInserted));
|
|
m_modelConnections.append(
|
|
QObject::connect(
|
|
m_model,
|
|
&QAbstractItemModel::rowsAboutToBeRemoved,
|
|
this,
|
|
&SnapView::onRowsAboutToBeRemoved));
|
|
m_modelConnections.append(
|
|
QObject::connect(
|
|
m_model,
|
|
&QAbstractItemModel::rowsRemoved,
|
|
this,
|
|
&SnapView::onRowsRemoved));
|
|
m_modelConnections.append(
|
|
QObject::connect(
|
|
m_model,
|
|
&QAbstractItemModel::dataChanged,
|
|
this,
|
|
&SnapView::onDataChanged));
|
|
m_modelConnections.append(
|
|
QObject::connect(
|
|
m_model,
|
|
&QAbstractItemModel::modelReset,
|
|
this,
|
|
&SnapView::onModelReset));
|
|
m_modelConnections.append(
|
|
QObject::connect(
|
|
m_model, &QAbstractItemModel::layoutChanged, this, [this]() {
|
|
rebuildRows();
|
|
polish();
|
|
}));
|
|
}
|
|
|
|
void SnapView::disconnectModel() {
|
|
for (const auto& conn : std::as_const(m_modelConnections))
|
|
disconnect(conn);
|
|
m_modelConnections.clear();
|
|
}
|
|
|
|
void SnapView::onRowsInserted(const QModelIndex& parent, int first, int last) {
|
|
if (parent.isValid()) return;
|
|
|
|
// Delegates at or after the inserted range now address a different row, and
|
|
// the inserted rows have no measured size yet.
|
|
shiftDelegateIndices(first, last - first + 1);
|
|
m_rowSizes.resize(count());
|
|
for (int index = first; index <= last; ++index)
|
|
m_rowSizes[index] = 0.0;
|
|
rebuildRows();
|
|
polish();
|
|
}
|
|
|
|
void SnapView::onRowsAboutToBeRemoved(
|
|
const QModelIndex& parent, int first, int last) {
|
|
if (parent.isValid()) return;
|
|
for (int index = first; index <= last; ++index)
|
|
destroyDelegate(index);
|
|
m_rowSizes.remove(first, last - first + 1);
|
|
shiftDelegateIndices(last + 1, -(last - first + 1));
|
|
}
|
|
|
|
void SnapView::onRowsRemoved(const QModelIndex& parent, int first, int last) {
|
|
if (parent.isValid()) return;
|
|
Q_UNUSED(first)
|
|
Q_UNUSED(last)
|
|
rebuildRows();
|
|
polish();
|
|
}
|
|
|
|
void SnapView::onDataChanged(
|
|
const QModelIndex& topLeft, const QModelIndex& bottomRight) {
|
|
if (topLeft.parent().isValid() || bottomRight.parent().isValid()) return;
|
|
for (int index = topLeft.row(); index <= bottomRight.row(); ++index)
|
|
updateDelegateData(index);
|
|
// A row's content can change its size, so re-place as well.
|
|
polish();
|
|
}
|
|
|
|
void SnapView::onModelReset() {
|
|
for (int index : m_items.keys())
|
|
destroyDelegate(index);
|
|
m_sizeDevCache.clear();
|
|
m_rowSizes.clear();
|
|
rebuildRows();
|
|
polish();
|
|
}
|
|
|
|
} // namespace ZShell::components
|