Files
z-bar-qt/Plugins/ZShell/Components/snapview.hpp
T
zach 11e7aac195
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
feat: better notification storage and image caching + blobs
2026-11-02 19:00:43 +01:00

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