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
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Caelestia.Components
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.config
|
import qs.config
|
||||||
|
|
||||||
Item {
|
LazyListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Props props
|
required property Props props
|
||||||
|
|
@ -16,19 +15,8 @@ Item {
|
||||||
required property Flickable container
|
required property Flickable container
|
||||||
required property DrawerVisibilities visibilities
|
required property DrawerVisibilities visibilities
|
||||||
|
|
||||||
readonly property real nonAnimHeight: {
|
readonly property real nonAnimHeight: layoutHeight
|
||||||
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 showAllNotifs
|
||||||
property bool flag
|
|
||||||
|
|
||||||
signal requestToggleExpand(expand: bool)
|
signal requestToggleExpand(expand: bool)
|
||||||
|
|
||||||
|
|
@ -42,7 +30,15 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
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 {
|
Timer {
|
||||||
id: clearTimer
|
id: clearTimer
|
||||||
|
|
@ -51,164 +47,116 @@ Item {
|
||||||
onTriggered: root.showAllNotifs = false
|
onTriggered: root.showAllNotifs = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
model: ScriptModel {
|
||||||
id: repeater
|
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
|
||||||
|
|
||||||
model: ScriptModel {
|
|
||||||
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
|
|
||||||
onValuesChanged: root.flagChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: NotifDelegate {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
delegate: Component {
|
||||||
Anim {
|
MouseArea {
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
id: notif
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component NotifDelegate: MouseArea {
|
required property int index
|
||||||
id: notif
|
required property NotifData modelData
|
||||||
|
|
||||||
required property int index
|
readonly property bool previewHidden: {
|
||||||
required property NotifData modelData
|
if (root.expanded)
|
||||||
|
|
||||||
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)))
|
|
||||||
return false;
|
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
|
Component.onCompleted: modelData?.lock(this)
|
||||||
scale: previewHidden ? 0.7 : 1
|
Component.onDestruction: modelData?.unlock(this)
|
||||||
|
|
||||||
implicitWidth: root.width
|
LazyListView.preferredHeight: modelData?.closed || previewHidden ? 0 : notifInner.nonAnimHeight
|
||||||
implicitHeight: notifInner.implicitHeight
|
LazyListView.visibleHeight: modelData?.closed || previewHidden ? 0 : notifInner.implicitHeight
|
||||||
|
implicitHeight: notifInner.implicitHeight
|
||||||
|
|
||||||
hoverEnabled: true
|
opacity: LazyListView.removing || modelData?.closed || previewHidden || LazyListView.adding ? 0 : 1
|
||||||
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
scale: LazyListView.removing || previewHidden ? 0.7 : LazyListView.adding ? 0.7 : 1
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
preventStealing: !root.expanded
|
|
||||||
enabled: !modelData.closed
|
|
||||||
|
|
||||||
drag.target: this
|
hoverEnabled: true
|
||||||
drag.axis: Drag.XAxis
|
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 => {
|
drag.target: this
|
||||||
startY = event.y;
|
drag.axis: Drag.XAxis
|
||||||
if (event.button === Qt.RightButton)
|
|
||||||
root.requestToggleExpand(!root.expanded);
|
onPressed: event => {
|
||||||
else if (event.button === Qt.MiddleButton)
|
startY = event.y;
|
||||||
modelData.close();
|
if (event.button === Qt.RightButton)
|
||||||
}
|
root.requestToggleExpand(!root.expanded);
|
||||||
onPositionChanged: event => {
|
else if (event.button === Qt.MiddleButton)
|
||||||
if (pressed && !root.expanded) {
|
modelData?.close();
|
||||||
const diffY = event.y - startY;
|
|
||||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
|
||||||
root.requestToggleExpand(diffY > 0);
|
|
||||||
}
|
}
|
||||||
}
|
onPositionChanged: event => {
|
||||||
onReleased: event => {
|
if (pressed && !root.expanded) {
|
||||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
const diffY = event.y - startY;
|
||||||
x = 0;
|
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||||
else
|
root.requestToggleExpand(diffY > 0);
|
||||||
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 {
|
onReleased: event => {
|
||||||
target: notif
|
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||||
property: "scale"
|
x = 0;
|
||||||
from: 0.7
|
else
|
||||||
to: 1
|
modelData?.close();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
ParallelAnimation {
|
||||||
running: notif.modelData.closed
|
running: notif.modelData?.closed ?? false
|
||||||
onFinished: notif.modelData.unlock(notif)
|
onFinished: notif.modelData?.unlock(notif)
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
target: notif
|
target: notif
|
||||||
property: "opacity"
|
property: "opacity"
|
||||||
to: 0
|
to: 0
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: notif
|
||||||
|
property: "x"
|
||||||
|
to: notif.x >= 0 ? notif.width : -notif.width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Anim {
|
|
||||||
target: notif
|
Notif {
|
||||||
property: "x"
|
id: notifInner
|
||||||
to: notif.x >= 0 ? notif.width : -notif.width
|
|
||||||
|
anchors.fill: parent
|
||||||
|
modelData: notif.modelData
|
||||||
|
props: root.props
|
||||||
|
expanded: root.expanded
|
||||||
|
visibilities: root.visibilities
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Notif {
|
Behavior on y {
|
||||||
id: notifInner
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
anchors.fill: parent
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
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 {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
}
|
||||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
|
||||||
|
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;
|
return m_contentHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qreal LazyListView::layoutHeight() const {
|
||||||
|
return m_layoutHeight;
|
||||||
|
}
|
||||||
|
|
||||||
qreal LazyListView::contentY() const {
|
qreal LazyListView::contentY() const {
|
||||||
return m_contentY;
|
return m_contentY;
|
||||||
}
|
}
|
||||||
|
|
@ -413,33 +417,55 @@ void LazyListView::updatePolish() {
|
||||||
// --- Layout Engine ---
|
// --- Layout Engine ---
|
||||||
|
|
||||||
void LazyListView::relayout() {
|
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;
|
qreal y = 0;
|
||||||
|
bool hasLayoutItem = false;
|
||||||
for (auto& record : m_layout) {
|
for (auto& record : m_layout) {
|
||||||
record.targetY = y;
|
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;
|
qreal visY = 0;
|
||||||
|
bool hasVisItem = false;
|
||||||
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) {
|
for (int i = 0; i < static_cast<int>(m_layout.size()); ++i) {
|
||||||
qreal h;
|
qreal h;
|
||||||
if (m_delegates.contains(i) && m_delegates[i].item)
|
if (m_delegates.contains(i) && m_delegates[i].item)
|
||||||
h = delegateVisibleHeight(m_delegates[i].item);
|
h = delegateVisibleHeight(m_delegates[i].item);
|
||||||
else
|
else
|
||||||
h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight();
|
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
|
// Account for dying delegates still visually present
|
||||||
for (const auto& dying : std::as_const(m_dyingDelegates)) {
|
for (const auto& dying : std::as_const(m_dyingDelegates)) {
|
||||||
if (dying.item)
|
if (!dying.item)
|
||||||
maxBottom = std::max(maxBottom, dying.item->y() + delegateVisibleHeight(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)) {
|
if (!qFuzzyCompare(m_contentHeight, visY)) {
|
||||||
m_contentHeight = maxBottom;
|
m_contentHeight = visY;
|
||||||
emit contentHeightChanged();
|
emit contentHeightChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -525,7 +551,9 @@ void LazyListView::syncDelegates() {
|
||||||
if (entry.item) {
|
if (entry.item) {
|
||||||
// Measure height (prefer attached preferredHeight, fall back to implicitHeight)
|
// Measure height (prefer attached preferredHeight, fall back to implicitHeight)
|
||||||
const qreal h = delegateHeight(entry.item);
|
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].height = h;
|
||||||
m_layout[i].heightKnown = true;
|
m_layout[i].heightKnown = true;
|
||||||
trackHeight(h);
|
trackHeight(h);
|
||||||
|
|
@ -625,6 +653,9 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
|
||||||
if (wasKnown)
|
if (wasKnown)
|
||||||
untrackHeight(oldH);
|
untrackHeight(oldH);
|
||||||
trackHeight(h);
|
trackHeight(h);
|
||||||
|
// Relayout immediately so layoutHeight/contentHeight update
|
||||||
|
// synchronously for parent bindings, then polish for delegate sync.
|
||||||
|
relayout();
|
||||||
polish();
|
polish();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ class LazyListView : public QQuickItem {
|
||||||
// Layout
|
// Layout
|
||||||
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
|
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
|
||||||
Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged)
|
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)
|
Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged)
|
||||||
|
|
||||||
// Viewport & Lazy Loading
|
// Viewport & Lazy Loading
|
||||||
|
|
@ -110,6 +111,7 @@ public:
|
||||||
void setSpacing(qreal spacing);
|
void setSpacing(qreal spacing);
|
||||||
|
|
||||||
[[nodiscard]] qreal contentHeight() const;
|
[[nodiscard]] qreal contentHeight() const;
|
||||||
|
[[nodiscard]] qreal layoutHeight() const;
|
||||||
|
|
||||||
[[nodiscard]] qreal contentY() const;
|
[[nodiscard]] qreal contentY() const;
|
||||||
void setContentY(qreal contentY);
|
void setContentY(qreal contentY);
|
||||||
|
|
@ -170,6 +172,7 @@ signals:
|
||||||
void delegateChanged();
|
void delegateChanged();
|
||||||
void spacingChanged();
|
void spacingChanged();
|
||||||
void contentHeightChanged();
|
void contentHeightChanged();
|
||||||
|
void layoutHeightChanged();
|
||||||
void contentYChanged();
|
void contentYChanged();
|
||||||
void viewportChanged();
|
void viewportChanged();
|
||||||
void useCustomViewportChanged();
|
void useCustomViewportChanged();
|
||||||
|
|
@ -248,6 +251,7 @@ private:
|
||||||
|
|
||||||
qreal m_spacing = 0;
|
qreal m_spacing = 0;
|
||||||
qreal m_contentHeight = 0;
|
qreal m_contentHeight = 0;
|
||||||
|
qreal m_layoutHeight = 0;
|
||||||
qreal m_contentY = 0;
|
qreal m_contentY = 0;
|
||||||
|
|
||||||
QRectF m_viewport;
|
QRectF m_viewport;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue