test notif plugin
This commit is contained in:
@@ -113,11 +113,12 @@ Singleton {
|
||||
id: storage
|
||||
|
||||
path: `${Paths.state}/notifs.json`
|
||||
printErrors: false
|
||||
|
||||
onLoadFailed: err => {
|
||||
if (err === FileViewError.FileNotFound) {
|
||||
root.loaded = true;
|
||||
setText("[]");
|
||||
Qt.callLater(() => setText("[]"));
|
||||
}
|
||||
}
|
||||
onLoaded: {
|
||||
|
||||
@@ -19,11 +19,12 @@ CustomRect {
|
||||
required property var visibilities
|
||||
|
||||
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);
|
||||
}
|
||||
implicitHeight: nonAnimHeight
|
||||
radius: 6
|
||||
state: expanded ? "expanded" : ""
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
@@ -33,7 +34,6 @@ CustomRect {
|
||||
}
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
|
||||
PropertyChanges {
|
||||
compactBody.anchors.margins: 10
|
||||
@@ -63,10 +63,10 @@ CustomRect {
|
||||
|
||||
anchors.left: parent.left
|
||||
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
|
||||
maximumLineCount: 1
|
||||
text: root.modelData.summary
|
||||
text: root.modelData?.summary ?? ""
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
@@ -76,7 +76,7 @@ CustomRect {
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text: root.modelData.summary
|
||||
text: root.modelData?.summary ?? ""
|
||||
visible: false
|
||||
}
|
||||
|
||||
@@ -90,9 +90,9 @@ CustomRect {
|
||||
shouldBeActive: !root.expanded
|
||||
|
||||
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
|
||||
text: root.modelData.body.replace(/\n/g, " ")
|
||||
text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
|
||||
textFormat: Text.StyledText
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ CustomRect {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
font.pointSize: 11
|
||||
text: root.modelData.timeStr
|
||||
text: root.modelData?.timeStr ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +130,8 @@ CustomRect {
|
||||
id: body
|
||||
|
||||
Layout.fillWidth: true
|
||||
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")
|
||||
color: root.modelData?.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface
|
||||
text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
|
||||
textFormat: Text.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
@@ -148,14 +148,51 @@ CustomRect {
|
||||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
id: comp
|
||||
|
||||
required property bool shouldBeActive
|
||||
|
||||
active: opacity > 0
|
||||
opacity: shouldBeActive ? 1 : 0
|
||||
active: false
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
states: State {
|
||||
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
|
||||
},
|
||||
...root.notif.actions,
|
||||
...(root.notif?.actions ?? ""),
|
||||
{
|
||||
isCopy: true
|
||||
}
|
||||
@@ -97,7 +97,7 @@ Item {
|
||||
id: actionInner
|
||||
|
||||
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 {
|
||||
|
||||
@@ -129,20 +129,27 @@ Item {
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
interval: 50
|
||||
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
let next = null;
|
||||
for (let i = 0; i < notifList.repeater.count; i++) {
|
||||
next = notifList.repeater.itemAt(i);
|
||||
if (!next?.closed)
|
||||
break;
|
||||
}
|
||||
if (next)
|
||||
next.closeAll();
|
||||
else
|
||||
const first = NotifServer.notClosed[0];
|
||||
if (!first) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,47 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Modules
|
||||
import qs.Daemons
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property Flickable container
|
||||
property bool flag
|
||||
required property Props props
|
||||
readonly property alias repeater: repeater
|
||||
readonly property int spacing: 8
|
||||
required property var visibilities
|
||||
required property DrawerVisibilities visibilities
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: {
|
||||
const item = repeater.itemAt(repeater.count - 1);
|
||||
return item ? item.y + item.implicitHeight : 0;
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
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()
|
||||
}
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
asynchronous: true
|
||||
cacheBuffer: 400
|
||||
implicitHeight: contentHeight
|
||||
readyDelay: 1
|
||||
removeDuration: Appearance.anim.durations.normal
|
||||
spacing: Appearance.spacing.small
|
||||
useCustomViewport: true
|
||||
viewport: Qt.rect(0, container.contentY, width, container.height)
|
||||
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
readonly property bool closed: notifInner.notifCount === 0
|
||||
required property int index
|
||||
required property string modelData
|
||||
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
|
||||
property int startY
|
||||
|
||||
function closeAll(): void {
|
||||
for (const n of NotifServer.notClosed.filter(n => n.appName === modelData)) {
|
||||
n.close();
|
||||
}
|
||||
clearTimer.start();
|
||||
}
|
||||
|
||||
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
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
||||
drag.axis: Drag.XAxis
|
||||
@@ -62,36 +49,32 @@ Item {
|
||||
enabled: !closed
|
||||
hoverEnabled: true
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
implicitWidth: root.width
|
||||
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
||||
preventStealing: true
|
||||
y: {
|
||||
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;
|
||||
}
|
||||
scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
|
||||
|
||||
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 {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,39 +99,22 @@ Item {
|
||||
closeAll();
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: true
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
target: notif
|
||||
to: 1
|
||||
}
|
||||
interval: 15
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
from: 0
|
||||
property: "scale"
|
||||
target: notif
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
onTriggered: {
|
||||
const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData);
|
||||
if (notifs.length === 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.closed
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: notif
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "scale"
|
||||
target: notif
|
||||
to: 0.6
|
||||
for (const n of notifs.slice(0, 30))
|
||||
n.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,4 +128,30 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const map = new Map();
|
||||
for (const n of Notifs.notClosed)
|
||||
map.set(n.appName, null);
|
||||
for (const n of Notifs.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ CustomRect {
|
||||
required property string modelData
|
||||
readonly property int nonAnimHeight: {
|
||||
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);
|
||||
}
|
||||
readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
|
||||
|
||||
@@ -8,113 +8,51 @@ import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property Flickable container
|
||||
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 Props props
|
||||
property bool showAllNotifs
|
||||
readonly property int spacing: Math.round(7 / 2)
|
||||
required property var visibilities
|
||||
required property DrawerVisibilities visibilities
|
||||
|
||||
signal requestToggleExpand(expand: bool)
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
}
|
||||
asynchronous: true
|
||||
cacheBuffer: 400
|
||||
implicitHeight: contentHeight
|
||||
readyDelay: 1
|
||||
removeDuration: Appearance.anim.durations.normal
|
||||
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: {
|
||||
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()
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
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;
|
||||
}
|
||||
required property NotifData modelData
|
||||
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
|
||||
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
||||
drag.axis: Drag.XAxis
|
||||
drag.target: this
|
||||
enabled: !modelData.closed
|
||||
enabled: !(modelData?.closed ?? true)
|
||||
hoverEnabled: true
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
implicitWidth: root.width
|
||||
opacity: previewHidden ? 0 : 1
|
||||
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
||||
preventStealing: !root.expanded
|
||||
scale: previewHidden ? 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;
|
||||
}
|
||||
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
|
||||
|
||||
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 {
|
||||
Anim {
|
||||
}
|
||||
@@ -125,19 +63,21 @@ Item {
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: modelData.lock(this)
|
||||
Component.onDestruction: modelData.unlock(this)
|
||||
Component.onCompleted: modelData?.lock(this)
|
||||
Component.onDestruction: modelData?.unlock(this)
|
||||
onPositionChanged: event => {
|
||||
if (pressed && !root.expanded) {
|
||||
const diffY = event.y - startY;
|
||||
@@ -150,37 +90,19 @@ Item {
|
||||
if (event.button === Qt.RightButton)
|
||||
root.requestToggleExpand(!root.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
modelData.close();
|
||||
modelData?.close();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
modelData.close();
|
||||
modelData?.close();
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
Component.onCompleted: running = !notif.previewHidden
|
||||
running: notif.modelData?.closed ?? false
|
||||
|
||||
Anim {
|
||||
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)
|
||||
onFinished: notif.modelData?.unlock(notif)
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
@@ -206,4 +128,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ CustomRect {
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
|
||||
implicitWidth: contentRow.implicitWidth + Appearance.spacing.smaller
|
||||
implicitWidth: contentRow.implicitWidth + Appearance.spacing.small * 2
|
||||
radius: height / 2
|
||||
|
||||
RowLayout {
|
||||
|
||||
@@ -56,3 +56,4 @@ qml_module(ZShell
|
||||
add_subdirectory(Models)
|
||||
add_subdirectory(Internal)
|
||||
add_subdirectory(Services)
|
||||
add_subdirectory(Components)
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user