Merge pull request 'test notif plugin' (#51) from notif-plugin into main

Reviewed-on: #51
This commit was merged in pull request #51.
This commit is contained in:
2026-04-12 22:04:23 +02:00
12 changed files with 1567 additions and 223 deletions
+2 -1
View File
@@ -113,11 +113,12 @@ Singleton {
id: storage id: storage
path: `${Paths.state}/notifs.json` path: `${Paths.state}/notifs.json`
printErrors: false
onLoadFailed: err => { onLoadFailed: err => {
if (err === FileViewError.FileNotFound) { if (err === FileViewError.FileNotFound) {
root.loaded = true; root.loaded = true;
setText("[]"); Qt.callLater(() => setText("[]"));
} }
} }
onLoaded: { onLoaded: {
+51 -14
View File
@@ -19,11 +19,12 @@ CustomRect {
required property var visibilities required property var visibilities
color: { color: {
const c = root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); const c = root.modelData?.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
return expanded ? c : Qt.alpha(c, 0); return expanded ? c : Qt.alpha(c, 0);
} }
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
radius: 6 radius: 6
state: expanded ? "expanded" : ""
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {
@@ -33,7 +34,6 @@ CustomRect {
} }
states: State { states: State {
name: "expanded" name: "expanded"
when: root.expanded
PropertyChanges { PropertyChanges {
compactBody.anchors.margins: 10 compactBody.anchors.margins: 10
@@ -63,10 +63,10 @@ CustomRect {
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface color: root.modelData?.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
text: root.modelData.summary text: root.modelData?.summary ?? ""
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
@@ -76,7 +76,7 @@ CustomRect {
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
text: root.modelData.summary text: root.modelData?.summary ?? ""
visible: false visible: false
} }
@@ -90,9 +90,9 @@ CustomRect {
shouldBeActive: !root.expanded shouldBeActive: !root.expanded
sourceComponent: CustomText { sourceComponent: CustomText {
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline color: root.modelData?.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline
elide: Text.ElideRight elide: Text.ElideRight
text: root.modelData.body.replace(/\n/g, " ") text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
textFormat: Text.StyledText textFormat: Text.StyledText
} }
} }
@@ -108,7 +108,7 @@ CustomRect {
animate: true animate: true
color: DynamicColors.palette.m3outline color: DynamicColors.palette.m3outline
font.pointSize: 11 font.pointSize: 11
text: root.modelData.timeStr text: root.modelData?.timeStr ?? ""
} }
} }
@@ -130,8 +130,8 @@ CustomRect {
id: body id: body
Layout.fillWidth: true Layout.fillWidth: true
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface color: root.modelData?.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface
text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given") text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
textFormat: Text.MarkdownText textFormat: Text.MarkdownText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -148,14 +148,51 @@ CustomRect {
} }
component WrappedLoader: Loader { component WrappedLoader: Loader {
id: comp
required property bool shouldBeActive required property bool shouldBeActive
active: opacity > 0 active: false
opacity: shouldBeActive ? 1 : 0 opacity: 0
Behavior on opacity { states: State {
Anim { name: "active"
when: comp.shouldBeActive
PropertyChanges {
comp.active: true
comp.opacity: 1
} }
} }
transitions: [
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
property: "active"
}
Anim {
property: "opacity"
}
}
},
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
property: "opacity"
}
PropertyAction {
property: "active"
}
}
}
]
} }
} }
@@ -35,7 +35,7 @@ Item {
{ {
isClose: true isClose: true
}, },
...root.notif.actions, ...(root.notif?.actions ?? ""),
{ {
isCopy: true isCopy: true
} }
@@ -97,7 +97,7 @@ Item {
id: actionInner id: actionInner
anchors.centerIn: parent anchors.centerIn: parent
sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp
} }
Component { Component {
+17 -10
View File
@@ -129,20 +129,27 @@ Item {
Timer { Timer {
id: clearTimer id: clearTimer
interval: 50 interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
repeat: true repeat: true
triggeredOnStart: true
onTriggered: { onTriggered: {
let next = null; const first = NotifServer.notClosed[0];
for (let i = 0; i < notifList.repeater.count; i++) { if (!first) {
next = notifList.repeater.itemAt(i);
if (!next?.closed)
break;
}
if (next)
next.closeAll();
else
stop(); stop();
return;
}
const appName = first.appName;
let cleared = 0;
for (const n of NotifServer.notClosed.filter(n => n.appName === appName)) {
n.close();
cleared++;
if (cleared > 30) {
interval = 5;
return;
}
}
} }
} }
+75 -83
View File
@@ -1,60 +1,47 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import ZShell.Components
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Modules import qs.Modules
import qs.Daemons import qs.Daemons
import Quickshell
import QtQuick
Item { LazyListView {
id: root id: root
required property Flickable container required property Flickable container
property bool flag
required property Props props required property Props props
readonly property alias repeater: repeater required property PersistentProperties visibilities
readonly property int spacing: 8
required property var visibilities
anchors.left: parent.left anchors.left: parent?.left
anchors.right: parent.right anchors.right: parent?.right
implicitHeight: { asynchronous: true
const item = repeater.itemAt(repeater.count - 1); cacheBuffer: 400
return item ? item.y + item.implicitHeight : 0; implicitHeight: contentHeight
} readyDelay: 1
removeDuration: Appearance.anim.durations.normal
Repeater { spacing: Appearance.spacing.small
id: repeater useCustomViewport: true
viewport: Qt.rect(0, container.contentY, width, container.height)
model: ScriptModel {
values: {
const map = new Map();
for (const n of NotifServer.notClosed)
map.set(n.appName, null);
for (const n of NotifServer.list)
map.set(n.appName, null);
return [...map.keys()];
}
onValuesChanged: root.flagChanged()
}
delegate: Component {
MouseArea { MouseArea {
id: notif id: notif
readonly property bool closed: notifInner.notifCount === 0 readonly property bool closed: notifInner.notifCount === 0
required property int index required property int index
required property string modelData required property string modelData
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
property int startY property int startY
function closeAll(): void { function closeAll(): void {
for (const n of NotifServer.notClosed.filter(n => n.appName === modelData)) { clearTimer.start();
n.close();
}
} }
LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight
LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight
LazyListView.visibleHeight: notifInner.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
cursorShape: pressed ? Qt.ClosedHandCursor : undefined cursorShape: pressed ? Qt.ClosedHandCursor : undefined
drag.axis: Drag.XAxis drag.axis: Drag.XAxis
@@ -62,36 +49,32 @@ Item {
enabled: !closed enabled: !closed
hoverEnabled: true hoverEnabled: true
implicitHeight: notifInner.implicitHeight implicitHeight: notifInner.implicitHeight
implicitWidth: root.width opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
preventStealing: true preventStealing: true
y: { scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (!item.closed)
y += item.nonAnimHeight + root.spacing;
}
return y;
}
containmentMask: QtObject { Behavior on opacity {
function contains(p: point): bool { Anim {
if (!root.container.contains(notif.mapToItem(root.container, p))) }
return false; }
return notifInner.contains(p); Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
} }
} }
Behavior on x { Behavior on x {
Anim { Anim {
duration: MaterialEasing.expressiveEffectsTime duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: MaterialEasing.expressiveEffects easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
} }
} }
Behavior on y { Behavior on y {
enabled: notif.LazyListView.ready
Anim { Anim {
duration: MaterialEasing.expressiveEffectsTime duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: MaterialEasing.expressiveEffects easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
} }
} }
@@ -116,39 +99,22 @@ Item {
closeAll(); closeAll();
} }
ParallelAnimation { Timer {
running: true id: clearTimer
Anim { interval: 15
from: 0 repeat: true
property: "opacity" triggeredOnStart: true
target: notif
to: 1
}
Anim { onTriggered: {
duration: MaterialEasing.expressiveEffectsTime const notifs = NotifServer.notClosed.filter(n => n.appName === notif.modelData);
easing.bezierCurve: MaterialEasing.expressiveEffects if (notifs.length === 0) {
from: 0 stop();
property: "scale" return;
target: notif }
to: 1
}
}
ParallelAnimation { for (const n of notifs.slice(0, 30))
running: notif.closed n.close();
Anim {
property: "opacity"
target: notif
to: 0
}
Anim {
property: "scale"
target: notif
to: 0.6
} }
} }
@@ -162,4 +128,30 @@ Item {
} }
} }
} }
model: ScriptModel {
values: {
const map = new Map();
for (const n of NotifServer.notClosed)
map.set(n.appName, null);
for (const n of NotifServer.list)
map.set(n.appName, null);
return [...map.keys()];
}
}
onViewportAdjustNeeded: d => {
if (contentYAnim.running)
contentYAnim.complete();
contentYAnim.to = Math.max(0, container.contentY + d);
contentYAnim.start();
}
Anim {
id: contentYAnim
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
property: "contentY"
target: root.container
}
} }
+1 -1
View File
@@ -20,7 +20,7 @@ CustomRect {
required property string modelData required property string modelData
readonly property int nonAnimHeight: { readonly property int nonAnimHeight: {
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(7 / 2) : 0); const headerHeight = header.implicitHeight + (root.expanded ? Math.round(7 / 2) : 0);
const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; const columnHeight = headerHeight + notifList.layoutHeight + column.Layout.topMargin + column.Layout.bottomMargin;
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + 10 * 2); return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + 10 * 2);
} }
readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
+58 -111
View File
@@ -1,120 +1,59 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Modules import qs.Modules
import qs.Daemons import qs.Daemons
import Quickshell
import QtQuick
import QtQuick.Layouts
Item { LazyListView {
id: root id: root
required property Flickable container required property Flickable container
required property bool expanded required property bool expanded
property bool flag
readonly property real nonAnimHeight: {
let h = -root.spacing;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i);
if (!item.modelData.closed && !item.previewHidden)
h += item.nonAnimHeight + root.spacing;
}
return h;
}
required property list<var> notifs required property list<var> notifs
required property Props props required property Props props
property bool showAllNotifs required property PersistentProperties visibilities
readonly property int spacing: Math.round(7 / 2)
required property var visibilities
signal requestToggleExpand(expand: bool) signal requestToggleExpand(expand: bool)
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: nonAnimHeight asynchronous: true
cacheBuffer: 400
Behavior on implicitHeight { implicitHeight: contentHeight
Anim { readyDelay: 1
duration: MaterialEasing.expressiveEffectsTime removeDuration: Appearance.anim.durations.normal
easing.bezierCurve: MaterialEasing.expressiveEffects spacing: Math.round(Appearance.spacing.small / 2)
} useCustomViewport: true
viewport: {
tWatcher.transform; // mapToItem is not reactive so use this to trigger updates
return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height);
} }
onExpandedChanged: { delegate: Component {
if (expanded) {
clearTimer.stop();
showAllNotifs = true;
} else {
clearTimer.start();
}
}
Timer {
id: clearTimer
interval: MaterialEasing.standardTime
onTriggered: root.showAllNotifs = false
}
Repeater {
id: repeater
model: ScriptModel {
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
onValuesChanged: root.flagChanged()
}
MouseArea { MouseArea {
id: notif id: notif
required property int index required property int index
required property NotifServer.Notif modelData required property NotifServer.Notif modelData
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
readonly property bool previewHidden: {
if (root.expanded)
return false;
let extraHidden = 0;
for (let i = 0; i < index; i++)
if (root.notifs[i].closed)
extraHidden++;
return index >= Config.notifs.groupPreviewNum + extraHidden;
}
property int startY property int startY
LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight
LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
drag.axis: Drag.XAxis drag.axis: Drag.XAxis
drag.target: this drag.target: this
enabled: !modelData.closed enabled: !(modelData?.closed ?? true)
hoverEnabled: true hoverEnabled: true
implicitHeight: notifInner.implicitHeight implicitHeight: notifInner.implicitHeight
implicitWidth: root.width opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
opacity: previewHidden ? 0 : 1
preventStealing: !root.expanded preventStealing: !root.expanded
scale: previewHidden ? 0.7 : 1 scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
y: {
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (!item.modelData.closed && !item.previewHidden)
y += item.nonAnimHeight + root.spacing;
}
return y;
}
containmentMask: QtObject {
function contains(p: point): bool {
if (!root.container.contains(notif.mapToItem(root.container, p)))
return false;
return notifInner.contains(p);
}
}
Behavior on opacity { Behavior on opacity {
Anim { Anim {
} }
@@ -125,19 +64,21 @@ Item {
} }
Behavior on x { Behavior on x {
Anim { Anim {
duration: MaterialEasing.expressiveEffectsTime duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: MaterialEasing.expressiveEffects easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
} }
} }
Behavior on y { Behavior on y {
enabled: notif.LazyListView.ready
Anim { Anim {
duration: MaterialEasing.expressiveEffectsTime duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: MaterialEasing.expressiveEffects easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
} }
} }
Component.onCompleted: modelData.lock(this) Component.onCompleted: modelData?.lock(this)
Component.onDestruction: modelData.unlock(this) Component.onDestruction: modelData?.unlock(this)
onPositionChanged: event => { onPositionChanged: event => {
if (pressed && !root.expanded) { if (pressed && !root.expanded) {
const diffY = event.y - startY; const diffY = event.y - startY;
@@ -150,37 +91,19 @@ Item {
if (event.button === Qt.RightButton) if (event.button === Qt.RightButton)
root.requestToggleExpand(!root.expanded); root.requestToggleExpand(!root.expanded);
else if (event.button === Qt.MiddleButton) else if (event.button === Qt.MiddleButton)
modelData.close(); modelData?.close();
} }
onReleased: event => { onReleased: event => {
if (Math.abs(x) < width * Config.notifs.clearThreshold) if (Math.abs(x) < width * Config.notifs.clearThreshold)
x = 0; x = 0;
else else
modelData.close(); modelData?.close();
} }
ParallelAnimation { ParallelAnimation {
Component.onCompleted: running = !notif.previewHidden running: notif.modelData?.closed ?? false
Anim { onFinished: notif.modelData?.unlock(notif)
from: 0
property: "opacity"
target: notif
to: 1
}
Anim {
from: 0.7
property: "scale"
target: notif
to: 1
}
}
ParallelAnimation {
running: notif.modelData.closed
onFinished: notif.modelData.unlock(notif)
Anim { Anim {
property: "opacity" property: "opacity"
@@ -206,4 +129,28 @@ Item {
} }
} }
} }
model: ScriptModel {
values: {
if (root.expanded)
return root.notifs;
let count = 0;
let i = 0;
const previewNum = Config.notifs.groupPreviewNum;
while (i < root.notifs.length && count < previewNum) {
if (!(root.notifs[i]?.closed ?? true))
count++;
i++;
}
return root.notifs.slice(0, i);
}
}
TransformWatcher {
id: tWatcher
a: root.container.contentItem
b: root
}
} }
+1 -1
View File
@@ -13,7 +13,7 @@ CustomRect {
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: contentRow.implicitWidth + Appearance.spacing.smaller implicitWidth: contentRow.implicitWidth + Appearance.spacing.small * 2
radius: height / 2 radius: height / 2
RowLayout { RowLayout {
+1
View File
@@ -56,3 +56,4 @@ qml_module(ZShell
add_subdirectory(Models) add_subdirectory(Models)
add_subdirectory(Internal) add_subdirectory(Internal)
add_subdirectory(Services) add_subdirectory(Services)
add_subdirectory(Components)
+7
View File
@@ -0,0 +1,7 @@
qml_module(ZShell-components
URI ZShell.Components
SOURCES
lazylistview.hpp lazylistview.cpp
LIBRARIES
Qt::Quick
)
File diff suppressed because it is too large Load Diff
+243
View File
@@ -0,0 +1,243 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qrect.h>
#include <qvector.h>
namespace ZShell::components {
class LazyListViewAttached : public QObject {
Q_OBJECT
Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged)
Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged)
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
Q_PROPERTY(bool adding READ adding NOTIFY addingChanged)
Q_PROPERTY(bool removing READ removing NOTIFY removingChanged)
Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged)
public:
explicit LazyListViewAttached(QObject* parent = nullptr);
[[nodiscard]] qreal preferredHeight() const;
void setPreferredHeight(qreal height);
[[nodiscard]] qreal visibleHeight() const;
void setVisibleHeight(qreal height);
[[nodiscard]] bool ready() const;
void setReady(bool ready);
[[nodiscard]] bool adding() const;
void setAdding(bool adding);
[[nodiscard]] bool removing() const;
void setRemoving(bool removing);
[[nodiscard]] bool trackViewport() const;
void setTrackViewport(bool track);
signals:
void preferredHeightChanged();
void visibleHeightChanged();
void readyChanged();
void addingChanged();
void removingChanged();
void trackViewportChanged();
private:
qreal m_preferredHeight = -1;
qreal m_visibleHeight = -1;
bool m_ready = false;
bool m_adding = false;
bool m_removing = false;
bool m_trackViewport = false;
};
class LazyListView : public QQuickItem {
Q_OBJECT
QML_ELEMENT
QML_ATTACHED(LazyListViewAttached)
// Model & Delegate
Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged)
Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
// Layout
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged)
Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged)
Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged)
// Viewport & Lazy Loading
Q_PROPERTY(QRectF viewport READ viewport WRITE setViewport NOTIFY viewportChanged)
Q_PROPERTY(bool useCustomViewport READ useCustomViewport WRITE setUseCustomViewport NOTIFY useCustomViewportChanged)
Q_PROPERTY(qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged)
// Sizing
Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged)
// Async
Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged)
// Animation Durations
Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged)
Q_PROPERTY(int readyDelay READ readyDelay WRITE setReadyDelay NOTIFY readyDelayChanged)
// State
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
explicit LazyListView(QQuickItem* parent = nullptr);
~LazyListView() override;
static LazyListViewAttached* qmlAttachedProperties(QObject* object);
// Model & Delegate
[[nodiscard]] QAbstractItemModel* model() const;
void setModel(QAbstractItemModel* model);
[[nodiscard]] QQmlComponent* delegate() const;
void setDelegate(QQmlComponent* delegate);
// Layout
[[nodiscard]] qreal spacing() const;
void setSpacing(qreal spacing);
[[nodiscard]] qreal contentHeight() const;
[[nodiscard]] qreal layoutHeight() const;
[[nodiscard]] qreal contentY() const;
void setContentY(qreal contentY);
// Viewport
[[nodiscard]] QRectF viewport() const;
void setViewport(const QRectF& viewport);
[[nodiscard]] bool useCustomViewport() const;
void setUseCustomViewport(bool use);
[[nodiscard]] qreal cacheBuffer() const;
void setCacheBuffer(qreal buffer);
// Sizing
[[nodiscard]] qreal estimatedHeight() const;
void setEstimatedHeight(qreal height);
// Async
[[nodiscard]] bool asynchronous() const;
void setAsynchronous(bool async);
// Animation Durations
[[nodiscard]] int removeDuration() const;
void setRemoveDuration(int duration);
[[nodiscard]] int readyDelay() const;
void setReadyDelay(int delay);
// State
[[nodiscard]] int count() const;
signals:
void modelChanged();
void delegateChanged();
void spacingChanged();
void contentHeightChanged();
void layoutHeightChanged();
void contentYChanged();
void viewportChanged();
void useCustomViewportChanged();
void cacheBufferChanged();
void estimatedHeightChanged();
void asynchronousChanged();
void removeDurationChanged();
void readyDelayChanged();
void countChanged();
void viewportAdjustNeeded(qreal delta);
protected:
void componentComplete() override;
void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override;
void updatePolish() override;
private:
struct ItemRecord {
qreal targetY = 0;
qreal height = 0;
bool heightKnown = false;
bool isNew = false;
};
struct DelegateEntry {
int modelIndex = -1;
QQuickItem* item = nullptr;
bool pendingRemoval = false;
bool pendingInsert = false;
bool readyDelayStarted = false;
};
// Layout
void relayout();
[[nodiscard]] std::pair<int, int> computeVisibleRange() const;
[[nodiscard]] QRectF effectiveViewport() const;
[[nodiscard]] qreal effectiveEstimatedHeight() const;
[[nodiscard]] static qreal delegateHeight(QQuickItem* item);
[[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item);
[[nodiscard]] static bool isDelegateReady(QQuickItem* item);
void trackHeight(qreal height);
void untrackHeight(qreal height);
// Delegate lifecycle
void syncDelegates();
DelegateEntry createDelegate(int modelIndex);
void destroyDelegate(DelegateEntry& entry);
void updateDelegateData(DelegateEntry& entry);
// Model connection
void connectModel();
void disconnectModel();
void resetContent();
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 onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row);
void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList<int>& roles);
void onModelReset();
// Members
QAbstractItemModel* m_model = nullptr;
QQmlComponent* m_delegate = nullptr;
qreal m_spacing = 0;
qreal m_contentHeight = 0;
qreal m_layoutHeight = 0;
qreal m_contentY = 0;
QRectF m_viewport;
bool m_useCustomViewport = false;
qreal m_cacheBuffer = 0;
qreal m_estimatedHeight = -1;
qreal m_knownHeightSum = 0;
int m_knownHeightCount = 0;
bool m_asynchronous = false;
int m_removeDuration = 300;
int m_readyDelay = 0;
QVector<ItemRecord> m_layout;
QHash<int, DelegateEntry> m_delegates;
QHash<QQuickItem*, int> m_itemToIndex;
QVector<DelegateEntry> m_dyingDelegates;
bool m_componentComplete = false;
bool m_relayoutPending = false;
QList<QMetaObject::Connection> m_modelConnections;
};
} // namespace ZShell::components