feat: replace notif group list with lazy list view
This commit is contained in:
parent
cf8b68be3b
commit
20983ffa6e
3 changed files with 146 additions and 163 deletions
|
|
@ -1,13 +1,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,19 +15,8 @@ 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)
|
||||
readonly property real nonAnimHeight: layoutHeight
|
||||
property bool showAllNotifs
|
||||
property bool flag
|
||||
|
||||
signal requestToggleExpand(expand: bool)
|
||||
|
||||
|
|
@ -42,7 +30,15 @@ Item {
|
|||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: nonAnimHeight
|
||||
implicitHeight: contentHeight
|
||||
|
||||
spacing: Math.round(Appearance.spacing.small / 2)
|
||||
|
||||
removeDuration: Appearance.anim.durations.normal
|
||||
|
||||
useCustomViewport: true
|
||||
viewport: Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y,
|
||||
width, container.height)
|
||||
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
|
@ -51,164 +47,116 @@ Item {
|
|||
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: NotifDelegate {}
|
||||
model: ScriptModel {
|
||||
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
component NotifDelegate: MouseArea {
|
||||
id: notif
|
||||
required property int index
|
||||
required property NotifData modelData
|
||||
|
||||
required property int index
|
||||
required property NotifData 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
|
||||
|
||||
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;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
containmentMask: QtObject {
|
||||
function contains(p: point): bool {
|
||||
if (!root.container.contains(notif.mapToItem(root.container, p)))
|
||||
readonly property bool previewHidden: {
|
||||
if (root.expanded)
|
||||
return false;
|
||||
return notifInner.contains(p);
|
||||
|
||||
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
|
||||
|
||||
opacity: previewHidden ? 0 : 1
|
||||
scale: previewHidden ? 0.7 : 1
|
||||
Component.onCompleted: modelData?.lock(this)
|
||||
Component.onDestruction: modelData?.unlock(this)
|
||||
|
||||
implicitWidth: root.width
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
LazyListView.preferredHeight: modelData?.closed || previewHidden ? 0 : notifInner.nonAnimHeight
|
||||
LazyListView.visibleHeight: modelData?.closed || previewHidden ? 0 : notifInner.implicitHeight
|
||||
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
|
||||
opacity: LazyListView.removing || modelData?.closed || previewHidden || LazyListView.adding ? 0 : 1
|
||||
scale: LazyListView.removing || previewHidden ? 0.7 : LazyListView.adding ? 0.7 : 1
|
||||
|
||||
drag.target: this
|
||||
drag.axis: Drag.XAxis
|
||||
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)
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
}
|
||||
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
|
||||
onPositionChanged: event => {
|
||||
if (pressed && !root.expanded) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
root.requestToggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "scale"
|
||||
from: 0.7
|
||||
to: 1
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
modelData?.close();
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.modelData.closed
|
||||
onFinished: notif.modelData.unlock(notif)
|
||||
ParallelAnimation {
|
||||
running: notif.modelData?.closed ?? false
|
||||
onFinished: notif.modelData?.unlock(notif)
|
||||
|
||||
Anim {
|
||||
target: notif
|
||||
property: "opacity"
|
||||
to: 0
|
||||
Anim {
|
||||
target: notif
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "x"
|
||||
to: notif.x >= 0 ? notif.width : -notif.width
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ qreal LazyListView::contentHeight() const {
|
|||
return m_contentHeight;
|
||||
}
|
||||
|
||||
qreal LazyListView::layoutHeight() const {
|
||||
return m_layoutHeight;
|
||||
}
|
||||
|
||||
qreal LazyListView::contentY() const {
|
||||
return m_contentY;
|
||||
}
|
||||
|
|
@ -413,33 +417,55 @@ void LazyListView::updatePolish() {
|
|||
// --- Layout Engine ---
|
||||
|
||||
void LazyListView::relayout() {
|
||||
// Layout positioning uses preferredHeight (final/non-animated)
|
||||
// Layout positioning uses preferredHeight (final/non-animated).
|
||||
// Only add spacing between items with non-zero height.
|
||||
qreal y = 0;
|
||||
bool hasLayoutItem = false;
|
||||
for (auto& record : m_layout) {
|
||||
record.targetY = y;
|
||||
y += (record.heightKnown ? record.height : effectiveEstimatedHeight()) + m_spacing;
|
||||
const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight();
|
||||
if (layoutH > 0) {
|
||||
if (hasLayoutItem)
|
||||
y += m_spacing;
|
||||
hasLayoutItem = true;
|
||||
y += layoutH;
|
||||
}
|
||||
}
|
||||
|
||||
// Content height tracks actual visible heights so scrolling follows animations
|
||||
if (!qFuzzyCompare(m_layoutHeight, y)) {
|
||||
m_layoutHeight = y;
|
||||
emit layoutHeightChanged();
|
||||
}
|
||||
|
||||
// Content height tracks actual visible heights so scrolling follows animations.
|
||||
// Only add spacing between items with non-zero visible height.
|
||||
qreal visY = 0;
|
||||
bool hasVisItem = false;
|
||||
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) {
|
||||
qreal h;
|
||||
if (m_delegates.contains(i) && m_delegates[i].item)
|
||||
h = delegateVisibleHeight(m_delegates[i].item);
|
||||
else
|
||||
h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight();
|
||||
visY += h + m_spacing;
|
||||
if (h > 0) {
|
||||
if (hasVisItem)
|
||||
visY += m_spacing;
|
||||
hasVisItem = true;
|
||||
visY += h;
|
||||
}
|
||||
}
|
||||
qreal maxBottom = m_layout.isEmpty() ? 0 : visY - m_spacing;
|
||||
|
||||
// Account for dying delegates still visually present
|
||||
for (const auto& dying : std::as_const(m_dyingDelegates)) {
|
||||
if (dying.item)
|
||||
maxBottom = std::max(maxBottom, dying.item->y() + delegateVisibleHeight(dying.item));
|
||||
if (!dying.item)
|
||||
continue;
|
||||
const qreal dyingH = delegateVisibleHeight(dying.item);
|
||||
if (dyingH > 0)
|
||||
visY = std::max(visY, dying.item->y() + dyingH);
|
||||
}
|
||||
|
||||
if (!qFuzzyCompare(m_contentHeight, maxBottom)) {
|
||||
m_contentHeight = maxBottom;
|
||||
if (!qFuzzyCompare(m_contentHeight, visY)) {
|
||||
m_contentHeight = visY;
|
||||
emit contentHeightChanged();
|
||||
}
|
||||
}
|
||||
|
|
@ -525,7 +551,9 @@ void LazyListView::syncDelegates() {
|
|||
if (entry.item) {
|
||||
// Measure height (prefer attached preferredHeight, fall back to implicitHeight)
|
||||
const qreal h = delegateHeight(entry.item);
|
||||
if (h > 0 && !m_layout[i].heightKnown) {
|
||||
if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) {
|
||||
if (m_layout[i].heightKnown)
|
||||
untrackHeight(m_layout[i].height);
|
||||
m_layout[i].height = h;
|
||||
m_layout[i].heightKnown = true;
|
||||
trackHeight(h);
|
||||
|
|
@ -625,6 +653,9 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
|
|||
if (wasKnown)
|
||||
untrackHeight(oldH);
|
||||
trackHeight(h);
|
||||
// Relayout immediately so layoutHeight/contentHeight update
|
||||
// synchronously for parent bindings, then polish for delegate sync.
|
||||
relayout();
|
||||
polish();
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class LazyListView : public QQuickItem {
|
|||
// 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
|
||||
|
|
@ -110,6 +111,7 @@ public:
|
|||
void setSpacing(qreal spacing);
|
||||
|
||||
[[nodiscard]] qreal contentHeight() const;
|
||||
[[nodiscard]] qreal layoutHeight() const;
|
||||
|
||||
[[nodiscard]] qreal contentY() const;
|
||||
void setContentY(qreal contentY);
|
||||
|
|
@ -170,6 +172,7 @@ signals:
|
|||
void delegateChanged();
|
||||
void spacingChanged();
|
||||
void contentHeightChanged();
|
||||
void layoutHeightChanged();
|
||||
void contentYChanged();
|
||||
void viewportChanged();
|
||||
void useCustomViewportChanged();
|
||||
|
|
@ -248,6 +251,7 @@ private:
|
|||
|
||||
qreal m_spacing = 0;
|
||||
qreal m_contentHeight = 0;
|
||||
qreal m_layoutHeight = 0;
|
||||
qreal m_contentY = 0;
|
||||
|
||||
QRectF m_viewport;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue