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
238 lines
7.9 KiB
C++
238 lines
7.9 KiB
C++
#pragma once
|
|
|
|
#include <QAbstractItemModel>
|
|
#include <QHash>
|
|
#include <QPointer>
|
|
#include <QQmlComponent>
|
|
#include <private/qquickflickable_p.h>
|
|
#include <private/qquickitem_p.h>
|
|
#include <qqmlintegration.h>
|
|
|
|
class QTimer;
|
|
|
|
namespace ZShell::components {
|
|
|
|
// A ListView replacement that lays delegates out the way SnapLayout lays out
|
|
// children: delegates are placed on device-pixel boundaries, the gaps between
|
|
// them are whole device pixels, siblings that move together keep their relative
|
|
// offsets, and a delegate that only moves is not resized. Like SnapLayout is to
|
|
// RowLayout/ColumnLayout, this is to ListView: a Flickable with its own
|
|
// placement, so there is no base-view positioner to fight with.
|
|
//
|
|
// Delegates receive `index` and `modelData` initial properties, so a delegate
|
|
// declares `required property int index` and `required property var modelData`.
|
|
// The cross-axis size (height in a horizontal view, width in a vertical one) is
|
|
// set by the view, as ListView does; the main-axis size comes from the delegate
|
|
// unless snapSizes is true.
|
|
class SnapView : public QQuickFlickable, public QQuickItemChangeListener {
|
|
Q_OBJECT
|
|
QML_ELEMENT
|
|
|
|
Q_PROPERTY(
|
|
QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged
|
|
FINAL)
|
|
Q_PROPERTY(
|
|
QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY
|
|
delegateChanged FINAL)
|
|
|
|
// Layout
|
|
Q_PROPERTY(
|
|
Qt::Orientation orientation READ orientation WRITE setOrientation NOTIFY
|
|
orientationChanged FINAL)
|
|
Q_PROPERTY(
|
|
qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged FINAL)
|
|
Q_PROPERTY(
|
|
qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY
|
|
cacheBufferChanged FINAL)
|
|
Q_PROPERTY(
|
|
qreal estimatedSize READ estimatedSize WRITE setEstimatedSize NOTIFY
|
|
estimatedSizeChanged FINAL)
|
|
|
|
// Resize delegates to device pixels along the main axis too. Off by
|
|
// default, because delegates usually derive that size from their content
|
|
// and resizing them would break the binding. Placement is always
|
|
// device-pixel snapped.
|
|
Q_PROPERTY(
|
|
bool snapSizes READ snapSizes WRITE setSnapSizes NOTIFY snapSizesChanged
|
|
FINAL)
|
|
|
|
// Spread delegate creation over frames instead of creating everything in
|
|
// the first polish pass.
|
|
Q_PROPERTY(
|
|
bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY
|
|
asynchronousChanged FINAL)
|
|
|
|
// State
|
|
Q_PROPERTY(int count READ count NOTIFY countChanged FINAL)
|
|
Q_PROPERTY(
|
|
int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY
|
|
currentIndexChanged FINAL)
|
|
Q_PROPERTY(
|
|
QQuickItem* currentItem READ currentItem NOTIFY currentItemChanged FINAL)
|
|
|
|
signals:
|
|
void modelChanged();
|
|
void delegateChanged();
|
|
void orientationChanged();
|
|
void spacingChanged();
|
|
void cacheBufferChanged();
|
|
void estimatedSizeChanged();
|
|
void snapSizesChanged();
|
|
void asynchronousChanged();
|
|
void countChanged();
|
|
void currentIndexChanged();
|
|
void currentItemChanged();
|
|
|
|
public:
|
|
explicit SnapView(QQuickItem* parent = nullptr);
|
|
~SnapView() override;
|
|
|
|
[[nodiscard]] QAbstractItemModel* model() const;
|
|
void setModel(QAbstractItemModel* model);
|
|
|
|
[[nodiscard]] QQmlComponent* delegate() const;
|
|
void setDelegate(QQmlComponent* delegate);
|
|
|
|
[[nodiscard]] Qt::Orientation orientation() const;
|
|
void setOrientation(Qt::Orientation orientation);
|
|
|
|
[[nodiscard]] qreal spacing() const;
|
|
void setSpacing(qreal spacing);
|
|
|
|
[[nodiscard]] qreal cacheBuffer() const;
|
|
void setCacheBuffer(qreal buffer);
|
|
|
|
[[nodiscard]] qreal estimatedSize() const;
|
|
void setEstimatedSize(qreal size);
|
|
|
|
[[nodiscard]] bool snapSizes() const;
|
|
void setSnapSizes(bool snapSizes);
|
|
|
|
[[nodiscard]] bool asynchronous() const;
|
|
void setAsynchronous(bool asynchronous);
|
|
|
|
[[nodiscard]] int count() const;
|
|
|
|
[[nodiscard]] int currentIndex() const;
|
|
void setCurrentIndex(int index);
|
|
|
|
[[nodiscard]] QQuickItem* currentItem() const;
|
|
|
|
Q_INVOKABLE void positionViewAtBeginning();
|
|
Q_INVOKABLE void positionViewAtEnd();
|
|
Q_INVOKABLE void positionViewAtIndex(int index);
|
|
|
|
protected:
|
|
void componentComplete() override;
|
|
void geometryChange(
|
|
const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
|
void updatePolish() override;
|
|
void itemChange(ItemChange change, const ItemChangeData& value) override;
|
|
|
|
// QQuickItemChangeListener
|
|
void itemGeometryChanged(
|
|
QQuickItem* item,
|
|
QQuickGeometryChange change,
|
|
const QRectF& oldGeometry) override;
|
|
void itemDestroyed(QQuickItem* item) override;
|
|
|
|
private:
|
|
struct Row {
|
|
qreal exactOffset = 0.0;
|
|
qreal exactSize = 0.0;
|
|
};
|
|
|
|
// Main-axis size last measured from a delegate, kept per model row so the
|
|
// content extent stays right while delegates outside the viewport are
|
|
// destroyed, the way ListView keeps contentHeight.
|
|
QVector<qreal> m_rowSizes;
|
|
|
|
// Placement, mirroring SnapLayout::arrangeChildren: exact positions come
|
|
// from cumulative row sizes, each near edge is rounded on its own so gaps
|
|
// stay whole device pixels, and siblings keep their relative offsets.
|
|
void layoutAndPlace();
|
|
void updateDelegates();
|
|
[[nodiscard]] QQuickItem* createDelegate(int index);
|
|
void destroyDelegate(int index);
|
|
void updateDelegateData(int index);
|
|
|
|
[[nodiscard]] qreal effectiveSpacing() const;
|
|
[[nodiscard]] qreal devicePixelRatio() const;
|
|
// Main-axis size of a row: the delegate's own size if it exists, otherwise
|
|
// the size cached when it was last visible, otherwise the estimate.
|
|
[[nodiscard]] qreal rowSize(int index) const;
|
|
[[nodiscard]] qint64 snappedDeviceMainSize(QQuickItem* item);
|
|
[[nodiscard]] int firstVisibleIndex() const;
|
|
[[nodiscard]] int lastVisibleIndex() const;
|
|
[[nodiscard]] QPointF contentOrigin() const;
|
|
void trackOriginResidual();
|
|
|
|
[[nodiscard]] QVariantMap delegateProperties(int index);
|
|
// The model's role names, cached because some models rebuild the hash on
|
|
// every call and this runs once per delegate per refresh.
|
|
[[nodiscard]] const QHash<int, QByteArray>& roleNames();
|
|
void invalidateRoleNames();
|
|
|
|
void rebuildRows();
|
|
// Sizes are measured along the main axis, so switching axis invalidates
|
|
// every cached size.
|
|
void invalidateMainAxisCaches();
|
|
// Re-keys the delegate map when rows shift, so an existing item keeps its
|
|
// place in the model instead of pointing at the wrong row.
|
|
void shiftDelegateIndices(int from, int delta);
|
|
void updateAncestorListeners();
|
|
void clearListeners();
|
|
void connectModel();
|
|
void disconnectModel();
|
|
|
|
void onRowsInserted(const QModelIndex& parent, int first, int last);
|
|
void onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last);
|
|
void onRowsRemoved(const QModelIndex& parent, int first, int last);
|
|
void onDataChanged(
|
|
const QModelIndex& topLeft, const QModelIndex& bottomRight);
|
|
void onModelReset();
|
|
|
|
QPointer<QAbstractItemModel> m_model;
|
|
QPointer<QQmlComponent> m_delegate;
|
|
|
|
Qt::Orientation m_orientation = Qt::Vertical;
|
|
qreal m_spacing = 0.0;
|
|
qreal m_cacheBuffer = 0.0;
|
|
qreal m_estimatedSize = -1.0;
|
|
bool m_snapSizes = false;
|
|
bool m_asynchronous = false;
|
|
|
|
int m_currentIndex = -1;
|
|
|
|
QVector<Row> m_rows;
|
|
QHash<int, QQuickItem*> m_items;
|
|
QVector<int> m_pendingCreate;
|
|
|
|
// Device-pixel main size of a delegate, so an item that only moves keeps
|
|
// the size it already has rather than re-rounding a drifting exact size.
|
|
QHash<QQuickItem*, qint64> m_sizeDevCache;
|
|
|
|
// Cached model role names; valid until the model or its roles change.
|
|
QHash<int, QByteArray> m_roleNames;
|
|
bool m_roleNamesValid = false;
|
|
|
|
// Fires the next asynchronous creation batch on the next event loop turn,
|
|
// so batches land on different frames instead of one polish flush.
|
|
QTimer* m_asyncTimer = nullptr;
|
|
|
|
// Items watched for sub-pixel moves. QPointer, as SnapLayout does, so a
|
|
// teardown order that destroys an item before the view cannot leave a
|
|
// dangling pointer to remove a listener from.
|
|
QVector<QPointer<QQuickItem>> m_delegateListeners;
|
|
QVector<QPointer<QQuickItem>> m_ancestorListeners;
|
|
qint64 m_originResidualX = -1;
|
|
qint64 m_originResidualY = -1;
|
|
|
|
bool m_placing = false;
|
|
bool m_componentComplete = false;
|
|
|
|
QList<QMetaObject::Connection> m_modelConnections;
|
|
};
|
|
|
|
} // namespace ZShell::components
|