Merge pull request #1375 from caelestia-dots/cxx-listview
Replace notif list inefficient fake list view with C++ lazy loaded list view
This commit is contained in:
commit
0e07176ff1
11 changed files with 1695 additions and 347 deletions
|
|
@ -22,13 +22,14 @@ StyledRect {
|
|||
|
||||
radius: Appearance.rounding.small
|
||||
color: {
|
||||
const c = root.modelData.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
|
||||
const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
|
||||
return expanded ? c : Qt.alpha(c, 0);
|
||||
}
|
||||
|
||||
state: expanded ? "expanded" : ""
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
|
||||
PropertyChanges {
|
||||
summary.anchors.margins: Appearance.padding.normal
|
||||
|
|
@ -61,8 +62,8 @@ StyledRect {
|
|||
anchors.left: parent.left
|
||||
|
||||
width: parent.width
|
||||
text: root.modelData.summary
|
||||
color: root.modelData.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
text: root.modelData?.summary ?? ""
|
||||
color: root.modelData?.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 1
|
||||
|
|
@ -75,7 +76,7 @@ StyledRect {
|
|||
anchors.left: parent.left
|
||||
|
||||
visible: false
|
||||
text: root.modelData.summary
|
||||
text: root.modelData?.summary ?? ""
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
|
|
@ -88,8 +89,8 @@ StyledRect {
|
|||
anchors.leftMargin: Appearance.spacing.small
|
||||
|
||||
sourceComponent: StyledText {
|
||||
text: root.modelData.body.replace(/\n/g, " ")
|
||||
color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline
|
||||
text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
|
||||
color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +104,7 @@ StyledRect {
|
|||
|
||||
sourceComponent: StyledText {
|
||||
animate: true
|
||||
text: root.modelData.timeStr
|
||||
text: root.modelData?.timeStr ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
|
@ -138,8 +139,8 @@ StyledRect {
|
|||
|
||||
Layout.fillWidth: true
|
||||
textFormat: Text.MarkdownText
|
||||
text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/")
|
||||
color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline
|
||||
text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/")
|
||||
color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: link => {
|
||||
|
|
@ -154,14 +155,51 @@ StyledRect {
|
|||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
id: comp
|
||||
|
||||
required property bool shouldBeActive
|
||||
|
||||
asynchronous: true
|
||||
opacity: shouldBeActive ? 1 : 0
|
||||
active: opacity > 0
|
||||
active: false
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||
states: State {
|
||||
name: "active"
|
||||
when: comp.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
comp.opacity: 1
|
||||
comp.active: true
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ Item {
|
|||
{
|
||||
isClose: true
|
||||
},
|
||||
...root.notif.actions,
|
||||
...(root.notif?.actions ?? []),
|
||||
{
|
||||
isCopy: true
|
||||
}
|
||||
|
|
@ -150,7 +150,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 {
|
||||
|
|
|
|||
|
|
@ -151,18 +151,24 @@ Item {
|
|||
id: clearTimer
|
||||
|
||||
repeat: true
|
||||
interval: 50
|
||||
triggeredOnStart: true
|
||||
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(Notifs.notClosed.length)))
|
||||
onTriggered: {
|
||||
let next = null;
|
||||
for (let i = 0; i < notifList.repeater.count; i++) {
|
||||
next = notifList.repeater.itemAt(i);
|
||||
if (!next?.closed) // qmllint disable missing-property
|
||||
break;
|
||||
}
|
||||
if (next) {
|
||||
next.closeAll(); // qmllint disable missing-property
|
||||
} else {
|
||||
const first = Notifs.notClosed[0];
|
||||
if (!first) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = first.appName;
|
||||
let cleared = 0;
|
||||
for (const n of Notifs.notClosed.filter(n => n.appName === appName)) {
|
||||
n.close();
|
||||
cleared++;
|
||||
if (cleared > 30) {
|
||||
interval = 5;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,168 +2,164 @@ pragma ComponentBehavior: Bound
|
|||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Caelestia.Components
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property Props props
|
||||
required property Flickable container
|
||||
required property DrawerVisibilities visibilities
|
||||
|
||||
readonly property alias repeater: repeater
|
||||
readonly property int spacing: Appearance.spacing.small
|
||||
property bool flag
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
implicitHeight: contentHeight
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: {
|
||||
const item = repeater.itemAt(repeater.count - 1);
|
||||
return item ? item.y + item.implicitHeight : 0;
|
||||
spacing: Appearance.spacing.small
|
||||
readyDelay: 1
|
||||
cacheBuffer: 400
|
||||
asynchronous: true
|
||||
|
||||
onViewportAdjustNeeded: d => {
|
||||
if (contentYAnim.running)
|
||||
contentYAnim.complete();
|
||||
contentYAnim.to = Math.max(0, container.contentY + d);
|
||||
contentYAnim.start();
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
useCustomViewport: true
|
||||
viewport: Qt.rect(0, container.contentY, width, container.height)
|
||||
|
||||
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()];
|
||||
}
|
||||
onValuesChanged: root.flagChanged()
|
||||
removeDuration: Appearance.anim.durations.normal
|
||||
|
||||
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()];
|
||||
}
|
||||
|
||||
delegate: NotifGroupDelegate {}
|
||||
}
|
||||
|
||||
component NotifGroupDelegate: MouseArea {
|
||||
id: notif
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property string modelData
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
readonly property bool closed: notifInner.notifCount === 0
|
||||
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
|
||||
property int startY
|
||||
readonly property bool closed: notifInner.notifCount === 0
|
||||
property int startY
|
||||
|
||||
function closeAll(): void {
|
||||
for (const n of Notifs.notClosed.filter(n => n.appName === modelData))
|
||||
n.close();
|
||||
}
|
||||
|
||||
y: {
|
||||
root.flag; // Force update
|
||||
let y = 0;
|
||||
for (let i = 0; i < index; i++) {
|
||||
const item = repeater.itemAt(i) as NotifGroupDelegate;
|
||||
if (item && !item.closed)
|
||||
y += item.nonAnimHeight + root.spacing;
|
||||
function closeAll(): void {
|
||||
clearTimer.start();
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
containmentMask: QtObject {
|
||||
function contains(p: point): bool {
|
||||
if (!root.container.contains(notif.mapToItem(root.container, p)))
|
||||
return false;
|
||||
return notifInner.contains(p);
|
||||
LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight
|
||||
LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight
|
||||
LazyListView.visibleHeight: notifInner.implicitHeight
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
|
||||
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
||||
scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
preventStealing: true
|
||||
enabled: !closed
|
||||
|
||||
drag.target: this
|
||||
drag.axis: Drag.XAxis
|
||||
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
notifInner.toggleExpand(!notifInner.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
closeAll();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
notifInner.toggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
closeAll();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
interval: 15
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData);
|
||||
if (notifs.length === 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const n of notifs.slice(0, 30))
|
||||
n.close();
|
||||
}
|
||||
}
|
||||
|
||||
NotifGroup {
|
||||
id: notifInner
|
||||
|
||||
modelData: notif.modelData
|
||||
props: root.props
|
||||
container: root.container
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: root.width
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
Anim {
|
||||
id: contentYAnim
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
preventStealing: true
|
||||
enabled: !closed
|
||||
|
||||
drag.target: this
|
||||
drag.axis: Drag.XAxis
|
||||
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
notifInner.toggleExpand(!notifInner.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
closeAll();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
notifInner.toggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
closeAll();
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: true
|
||||
|
||||
Anim {
|
||||
target: notif
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "scale"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.closed
|
||||
|
||||
Anim {
|
||||
target: notif
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "scale"
|
||||
to: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
NotifGroup {
|
||||
id: notifInner
|
||||
|
||||
modelData: notif.modelData
|
||||
props: root.props
|
||||
container: root.container
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
target: root.container
|
||||
property: "contentY"
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ StyledRect {
|
|||
|
||||
readonly property int nonAnimHeight: {
|
||||
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 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) + Appearance.padding.normal * 2);
|
||||
}
|
||||
readonly property bool expanded: props.expandedNotifs.includes(modelData)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ pragma ComponentBehavior: Bound
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Caelestia.Components
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property Props props
|
||||
|
|
@ -16,200 +17,147 @@ Item {
|
|||
required property Flickable container
|
||||
required property DrawerVisibilities visibilities
|
||||
|
||||
readonly property real nonAnimHeight: {
|
||||
let h = -root.spacing;
|
||||
for (let i = 0; i < repeater.count; i++) {
|
||||
const item = repeater.itemAt(i) as NotifDelegate;
|
||||
if (item && !item.modelData.closed && !item.previewHidden)
|
||||
h += item.nonAnimHeight + root.spacing;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
readonly property int spacing: Math.round(Appearance.spacing.small / 2)
|
||||
property bool showAllNotifs
|
||||
property bool flag
|
||||
|
||||
signal requestToggleExpand(expand: bool)
|
||||
|
||||
onExpandedChanged: {
|
||||
if (expanded) {
|
||||
clearTimer.stop();
|
||||
showAllNotifs = true;
|
||||
} else {
|
||||
clearTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: nonAnimHeight
|
||||
implicitHeight: contentHeight
|
||||
|
||||
Timer {
|
||||
id: clearTimer
|
||||
spacing: Math.round(Appearance.spacing.small / 2)
|
||||
asynchronous: true
|
||||
|
||||
interval: Appearance.anim.durations.normal
|
||||
onTriggered: root.showAllNotifs = false
|
||||
readyDelay: 1
|
||||
cacheBuffer: 400
|
||||
removeDuration: Appearance.anim.durations.normal
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
|
||||
onValuesChanged: root.flagChanged()
|
||||
}
|
||||
|
||||
delegate: NotifDelegate {}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
component NotifDelegate: MouseArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property NotifData modelData
|
||||
|
||||
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
|
||||
readonly property bool previewHidden: {
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
if (root.expanded)
|
||||
return false;
|
||||
return root.notifs;
|
||||
|
||||
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
|
||||
|
||||
y: {
|
||||
root.flag; // Force update
|
||||
let y = 0;
|
||||
for (let i = 0; i < index; i++) {
|
||||
const item = repeater.itemAt(i) as NotifDelegate;
|
||||
if (item && !item.modelData.closed && !item.previewHidden)
|
||||
y += item.nonAnimHeight + root.spacing;
|
||||
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 y;
|
||||
}
|
||||
|
||||
containmentMask: QtObject {
|
||||
function contains(p: point): bool {
|
||||
if (!root.container.contains(notif.mapToItem(root.container, p)))
|
||||
return false;
|
||||
return notifInner.contains(p);
|
||||
return root.notifs.slice(0, i);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property NotifData modelData
|
||||
|
||||
property int startY
|
||||
|
||||
Component.onCompleted: modelData?.lock(this)
|
||||
Component.onDestruction: modelData?.unlock(this)
|
||||
|
||||
LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight
|
||||
LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
|
||||
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
||||
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
preventStealing: !root.expanded
|
||||
enabled: !(modelData?.closed ?? true)
|
||||
|
||||
drag.target: this
|
||||
drag.axis: Drag.XAxis
|
||||
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
root.requestToggleExpand(!root.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
modelData?.close();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed && !root.expanded) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
root.requestToggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
modelData?.close();
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.modelData?.closed ?? false
|
||||
onFinished: notif.modelData?.unlock(notif)
|
||||
|
||||
Anim {
|
||||
target: notif
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "x"
|
||||
to: notif.x >= 0 ? notif.width : -notif.width
|
||||
}
|
||||
}
|
||||
|
||||
Notif {
|
||||
id: notifInner
|
||||
|
||||
anchors.fill: parent
|
||||
modelData: notif.modelData
|
||||
props: root.props
|
||||
expanded: root.expanded
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opacity: previewHidden ? 0 : 1
|
||||
scale: previewHidden ? 0.7 : 1
|
||||
TransformWatcher {
|
||||
id: tWatcher
|
||||
|
||||
implicitWidth: root.width
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
preventStealing: !root.expanded
|
||||
enabled: !modelData.closed
|
||||
|
||||
drag.target: this
|
||||
drag.axis: Drag.XAxis
|
||||
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
root.requestToggleExpand(!root.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
modelData.close();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed && !root.expanded) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
root.requestToggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
modelData.close();
|
||||
}
|
||||
|
||||
Component.onCompleted: modelData.lock(this)
|
||||
Component.onDestruction: modelData.unlock(this)
|
||||
|
||||
ParallelAnimation {
|
||||
Component.onCompleted: running = !notif.previewHidden
|
||||
|
||||
Anim {
|
||||
target: notif
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "scale"
|
||||
from: 0.7
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.modelData.closed
|
||||
onFinished: notif.modelData.unlock(notif)
|
||||
|
||||
Anim {
|
||||
target: notif
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "x"
|
||||
to: notif.x >= 0 ? notif.width : -notif.width
|
||||
}
|
||||
}
|
||||
|
||||
Notif {
|
||||
id: notifInner
|
||||
|
||||
anchors.fill: parent
|
||||
modelData: notif.modelData
|
||||
props: root.props
|
||||
expanded: root.expanded
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
a: root.container.contentItem
|
||||
b: root
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ qml_module(caelestia
|
|||
PkgConfig::Qalculate
|
||||
)
|
||||
|
||||
add_subdirectory(Components)
|
||||
add_subdirectory(Internal)
|
||||
add_subdirectory(Models)
|
||||
add_subdirectory(Services)
|
||||
|
|
|
|||
7
plugin/src/Caelestia/Components/CMakeLists.txt
Normal file
7
plugin/src/Caelestia/Components/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
qml_module(caelestia-components
|
||||
URI Caelestia.Components
|
||||
SOURCES
|
||||
lazylistview.hpp lazylistview.cpp
|
||||
LIBRARIES
|
||||
Qt::Quick
|
||||
)
|
||||
1108
plugin/src/Caelestia/Components/lazylistview.cpp
Normal file
1108
plugin/src/Caelestia/Components/lazylistview.cpp
Normal file
File diff suppressed because it is too large
Load diff
243
plugin/src/Caelestia/Components/lazylistview.hpp
Normal file
243
plugin/src/Caelestia/Components/lazylistview.hpp
Normal 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 caelestia::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 caelestia::components
|
||||
|
|
@ -105,6 +105,7 @@ Singleton {
|
|||
FileView {
|
||||
id: storage
|
||||
|
||||
printErrors: false
|
||||
path: `${Paths.state}/notifs.json`
|
||||
onLoaded: {
|
||||
const data = JSON.parse(text());
|
||||
|
|
@ -116,7 +117,7 @@ Singleton {
|
|||
onLoadFailed: err => {
|
||||
if (err === FileViewError.FileNotFound) {
|
||||
root.loaded = true;
|
||||
setText("[]");
|
||||
Qt.callLater(() => setText("[]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue