notif actions rework
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
|
||||
ButtonBase {
|
||||
id: root
|
||||
|
||||
readonly property alias label: label
|
||||
property alias text: label.text
|
||||
|
||||
activeColor: type === TextButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
|
||||
activeOnColor: {
|
||||
if (type === TextButton.Text)
|
||||
return DynamicColors.palette.m3primary;
|
||||
return type === TextButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondary;
|
||||
}
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
implicitHeight: label.implicitHeight + verticalPadding * 2
|
||||
implicitWidth: label.implicitWidth + horizontalPadding * 2
|
||||
inactiveColor: {
|
||||
if (!isToggle && type === TextButton.Filled)
|
||||
return DynamicColors.palette.m3primary;
|
||||
return type === TextButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer;
|
||||
}
|
||||
inactiveOnColor: {
|
||||
if (!isToggle && type === TextButton.Filled)
|
||||
return DynamicColors.palette.m3onPrimary;
|
||||
if (type === TextButton.Text)
|
||||
return DynamicColors.palette.m3primary;
|
||||
return type === TextButton.Filled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer;
|
||||
}
|
||||
verticalPadding: Appearance.padding.small
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.onColor
|
||||
}
|
||||
}
|
||||
@@ -215,6 +215,10 @@ Singleton {
|
||||
notif.hasActionIcons = notif.notification.hasActionIcons;
|
||||
}
|
||||
|
||||
function onHintsChanged(): void {
|
||||
notif.hints = notif.notification.hints;
|
||||
}
|
||||
|
||||
function onImageChanged(): void {
|
||||
notif.imageSource = notif.notification.image || "";
|
||||
notif.image = notif.imageSource;
|
||||
@@ -237,6 +241,7 @@ Singleton {
|
||||
}
|
||||
property real expireTimeout: 5
|
||||
property bool hasActionIcons
|
||||
property var hints
|
||||
property string image
|
||||
property string imageSource
|
||||
property var locks: new Set()
|
||||
@@ -333,6 +338,7 @@ Singleton {
|
||||
appName = notification.appName;
|
||||
imageSource = notification.image || "";
|
||||
image = imageSource;
|
||||
hints = notification.hints;
|
||||
expireTimeout = notification.expireTimeout;
|
||||
urgency = notification.urgency;
|
||||
resident = notification.resident;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Daemons
|
||||
@@ -149,11 +151,12 @@ Item {
|
||||
color: "transparent"
|
||||
implicitHeight: notif.implicitHeight
|
||||
implicitWidth: notif.implicitWidth
|
||||
radius: Appearance.rounding.smallest / 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Notification {
|
||||
id: notif
|
||||
|
||||
implicitWidth: root.implicitWidth - root.padding * 2
|
||||
modelData: wrapper.modelData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Modules
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Components
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText
|
||||
property bool expanded: Config.notifs.openExpanded
|
||||
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
||||
readonly property bool hasImage: modelData.image.length > 0
|
||||
required property NotifServer.Notif modelData
|
||||
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
|
||||
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Appearance.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
|
||||
|
||||
clip: true
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondaryContainer : DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: inner.implicitHeight
|
||||
implicitWidth: Config.notifs.sizes.width
|
||||
radius: 6
|
||||
x: Config.notifs.sizes.width
|
||||
radius: Appearance.rounding.small
|
||||
x: implicitWidth
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +53,7 @@ CustomRect {
|
||||
return;
|
||||
|
||||
const actions = root.modelData.actions;
|
||||
if (actions?.length === 1)
|
||||
if (actions.length === 1)
|
||||
actions[0].invoke();
|
||||
}
|
||||
onEntered: root.modelData.timer.stop()
|
||||
@@ -80,7 +78,7 @@ CustomRect {
|
||||
if (!containsMouse)
|
||||
root.modelData.timer.start();
|
||||
|
||||
if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold)
|
||||
if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold)
|
||||
root.x = 0;
|
||||
else
|
||||
root.modelData.popup = false;
|
||||
@@ -90,7 +88,7 @@ CustomRect {
|
||||
id: inner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 8
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: root.nonAnimHeight
|
||||
@@ -111,7 +109,8 @@ CustomRect {
|
||||
visible: root.hasImage || root.hasAppIcon
|
||||
width: Config.notifs.sizes.image
|
||||
|
||||
sourceComponent: ClippingRectangle {
|
||||
sourceComponent: CustomClippingRect {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) : DynamicColors.palette.m3secondaryContainer
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
radius: Appearance.rounding.full
|
||||
@@ -121,8 +120,11 @@ CustomRect {
|
||||
asynchronous: true
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
mipmap: true
|
||||
source: Qt.resolvedUrl(root.modelData.image)
|
||||
sourceSize: {
|
||||
const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
||||
return Qt.size(size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,8 +154,9 @@ CustomRect {
|
||||
height: Math.round(parent.width * 0.6)
|
||||
width: Math.round(parent.width * 0.6)
|
||||
|
||||
sourceComponent: CustomIcon {
|
||||
sourceComponent: ColoredIcon {
|
||||
anchors.fill: parent
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer
|
||||
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.modelData.appIcon)
|
||||
}
|
||||
@@ -162,34 +165,67 @@ CustomRect {
|
||||
Loader {
|
||||
active: !root.hasAppIcon
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -18 * 0.02
|
||||
anchors.verticalCenterOffset: 18 * 0.02
|
||||
anchors.verticalCenterOffset: 1
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer
|
||||
font.pointSize: 18
|
||||
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: progressIndicator
|
||||
|
||||
anchors.centerIn: appIcon
|
||||
height: appIcon.implicitHeight + progressShape.strokeWidth * 2
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
width: appIcon.implicitWidth + progressShape.strokeWidth * 2
|
||||
|
||||
ShapePath {
|
||||
id: progressShape
|
||||
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
strokeColor: DynamicColors.palette.m3primary
|
||||
strokeWidth: 2
|
||||
|
||||
PathAngleArc {
|
||||
id: progressArc
|
||||
|
||||
centerX: progressIndicator.width / 2
|
||||
centerY: progressIndicator.height / 2
|
||||
radiusX: progressIndicator.width / 2 - Appearance.padding.extraSmall / 2
|
||||
radiusY: progressIndicator.height / 2 - Appearance.padding.extraSmall / 2
|
||||
startAngle: -90
|
||||
sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360
|
||||
|
||||
Behavior on sweepAngle {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: appName
|
||||
|
||||
anchors.left: image.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.top: parent.top
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: 10
|
||||
maximumLineCount: 1
|
||||
opacity: root.expanded ? 1 : 0
|
||||
text: appNameMetrics.elidedText
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,9 +234,8 @@ CustomRect {
|
||||
id: appNameMetrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
|
||||
font.family: appName.font.family
|
||||
font.pointSize: appName.font.pointSize
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
|
||||
font: appName.font
|
||||
text: root.modelData.appName
|
||||
}
|
||||
|
||||
@@ -208,7 +243,7 @@ CustomRect {
|
||||
id: summary
|
||||
|
||||
anchors.left: image.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.top: parent.top
|
||||
animate: true
|
||||
height: implicitHeight
|
||||
@@ -224,6 +259,9 @@ CustomRect {
|
||||
when: root.expanded
|
||||
|
||||
PropertyChanges {
|
||||
body.anchors.topMargin: Appearance.spacing.extraSmall
|
||||
bodyPreview.anchors.topMargin: Appearance.spacing.extraSmall
|
||||
summary.anchors.topMargin: Appearance.spacing.extraSmall
|
||||
summary.maximumLineCount: undefined
|
||||
}
|
||||
|
||||
@@ -238,10 +276,11 @@ CustomRect {
|
||||
target: summary
|
||||
}
|
||||
|
||||
AnchorAnimation {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
easing.type: Easing.BezierSpline
|
||||
Anim {
|
||||
property: "topMargin"
|
||||
}
|
||||
|
||||
AnchorAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,9 +289,8 @@ CustomRect {
|
||||
id: summaryMetrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
|
||||
font.family: summary.font.family
|
||||
font.pointSize: summary.font.pointSize
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
|
||||
font: summary.font
|
||||
text: root.modelData.summary
|
||||
}
|
||||
|
||||
@@ -260,10 +298,9 @@ CustomRect {
|
||||
id: timeSep
|
||||
|
||||
anchors.left: summary.right
|
||||
anchors.leftMargin: 7
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.top: parent.top
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: 10
|
||||
text: "•"
|
||||
|
||||
states: State {
|
||||
@@ -276,10 +313,7 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
easing.type: Easing.BezierSpline
|
||||
AnchorAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,11 +322,10 @@ CustomRect {
|
||||
id: time
|
||||
|
||||
anchors.left: timeSep.right
|
||||
anchors.leftMargin: 7
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.top: parent.top
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: 10
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: root.modelData.timeStr
|
||||
}
|
||||
@@ -302,25 +335,33 @@ CustomRect {
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: expandIcon.height
|
||||
implicitWidth: expandIcon.height
|
||||
anchors.topMargin: -Appearance.padding.extraSmall
|
||||
implicitHeight: expandIcon.implicitHeight
|
||||
implicitWidth: expandIcon.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
onClicked: {
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
onClicked: root.expanded = !root.expanded
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: expandIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
font.pointSize: 13
|
||||
text: root.expanded ? "expand_less" : "expand_more"
|
||||
anchors.verticalCenterOffset: root.expanded ? -1 : 1
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on rotation {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,17 +370,17 @@ CustomRect {
|
||||
|
||||
anchors.left: summary.left
|
||||
anchors.right: expandBtn.left
|
||||
anchors.rightMargin: 7
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
anchors.top: summary.bottom
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: 10
|
||||
opacity: root.expanded ? 0 : 1
|
||||
text: bodyPreviewMetrics.elidedText
|
||||
textFormat: Text.MarkdownText
|
||||
textFormat: root.bodyTextFormat
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,8 +390,7 @@ CustomRect {
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: bodyPreview.width
|
||||
font.family: bodyPreview.font.family
|
||||
font.pointSize: bodyPreview.font.pointSize
|
||||
font: bodyPreview.font
|
||||
text: root.modelData.body
|
||||
}
|
||||
|
||||
@@ -359,19 +399,19 @@ CustomRect {
|
||||
|
||||
anchors.left: summary.left
|
||||
anchors.right: expandBtn.left
|
||||
anchors.rightMargin: 7
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
anchors.top: summary.bottom
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: 10
|
||||
height: text ? implicitHeight : 0
|
||||
opacity: root.expanded ? 1 : 0
|
||||
text: root.modelData.body
|
||||
textFormat: Text.MarkdownText
|
||||
textFormat: root.bodyTextFormat
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,84 +424,80 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ButtonRow {
|
||||
id: actions
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.left: body.left
|
||||
anchors.right: body.right
|
||||
anchors.top: body.bottom
|
||||
anchors.topMargin: 7
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
opacity: root.expanded ? 1 : 0
|
||||
spacing: 10
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
modelData: QtObject {
|
||||
readonly property string text: qsTr("Close")
|
||||
IconButton {
|
||||
fillWidth: root.modelData.actions.length === 0
|
||||
icon: "close"
|
||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
padding: Appearance.padding.extraSmall
|
||||
shapeMorph: true
|
||||
|
||||
function invoke(): void {
|
||||
root.modelData.close();
|
||||
}
|
||||
}
|
||||
onClicked: root.modelData.close()
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.modelData.actions
|
||||
|
||||
delegate: Component {
|
||||
Action {
|
||||
}
|
||||
TextButton {
|
||||
required property var modelData
|
||||
|
||||
fillWidth: true
|
||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
label.anchors.centerIn: undefined
|
||||
label.anchors.left: left
|
||||
label.anchors.margins: Appearance.padding.normal
|
||||
label.anchors.right: right
|
||||
label.anchors.verticalCenter: verticalCenter
|
||||
label.elide: Text.ElideRight
|
||||
label.horizontalAlignment: Text.AlignHCenter
|
||||
shapeMorph: true
|
||||
text: modelData.text
|
||||
|
||||
onClicked: modelData.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
fillWidth: root.modelData.actions.length === 0
|
||||
icon: copyTimer.running ? "inventory" : "content_copy"
|
||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
label.animate: true
|
||||
padding: Appearance.padding.extraSmall
|
||||
shapeMorph: true
|
||||
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = root.modelData.body;
|
||||
copyTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
|
||||
interval: 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Action: CustomRect {
|
||||
id: action
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredHeight: actionText.height + 4 * 2
|
||||
Layout.preferredWidth: actionText.width + 8 * 2
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: actionText.height + 4 * 2
|
||||
implicitWidth: actionText.width + 8 * 2
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
StateLayer {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurface
|
||||
enabled: root.expanded
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
onClicked: {
|
||||
action.modelData.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: actionText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: 10
|
||||
text: actionTextMetrics.elidedText
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: actionTextMetrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: {
|
||||
const numActions = root.modelData.actions.length + 1;
|
||||
return (inner.width - actions.spacing * (numActions - 1)) / numActions - 8 * 2;
|
||||
}
|
||||
font.family: actionText.font.family
|
||||
font.pointSize: actionText.font.pointSize
|
||||
text: action.modelData.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ qml_module(ZShell-components
|
||||
SOURCES
|
||||
lazylistview.hpp lazylistview.cpp
|
||||
wavyline.hpp wavyline.cpp
|
||||
buttonrow.hpp buttonrow.cpp
|
||||
LIBRARIES
|
||||
Qt::Quick
|
||||
)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "buttonrow.hpp"
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
ButtonRow::ButtonRow(QQuickItem* parent)
|
||||
: QQuickItem(parent)
|
||||
, m_dirty(false)
|
||||
, m_spacing(0.0) {
|
||||
setFlag(QQuickItem::ItemHasContents, true);
|
||||
QObject::connect(this, &ButtonRow::widthChanged, this, &ButtonRow::invalidate);
|
||||
}
|
||||
|
||||
qreal ButtonRow::spacing() const {
|
||||
return m_spacing;
|
||||
}
|
||||
|
||||
void ButtonRow::setSpacing(qreal spacing) {
|
||||
if (qFuzzyCompare(m_spacing + 1.0, spacing + 1.0))
|
||||
return;
|
||||
|
||||
m_spacing = spacing;
|
||||
emit spacingChanged();
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void ButtonRow::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) {
|
||||
if (change == QQuickItem::ItemChildAddedChange) {
|
||||
auto* const child = data.item;
|
||||
QObject::connect(child, &QQuickItem::implicitWidthChanged, this, &ButtonRow::invalidate);
|
||||
QObject::connect(child, &QQuickItem::implicitHeightChanged, this, &ButtonRow::invalidate);
|
||||
QObject::connect(child, &QQuickItem::visibleChanged, this, &ButtonRow::invalidate);
|
||||
|
||||
const auto* childMeta = child->metaObject();
|
||||
const auto morphSignalIdx = childMeta->indexOfSignal("shapeMorphExpansionChanged()");
|
||||
if (morphSignalIdx != -1)
|
||||
QObject::connect(child, childMeta->method(morphSignalIdx), this,
|
||||
metaObject()->method(metaObject()->indexOfSlot("invalidate()")));
|
||||
|
||||
invalidate();
|
||||
} else if (change == QQuickItem::ItemChildRemovedChange) {
|
||||
QObject::disconnect(data.item, nullptr, this, nullptr);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
QQuickItem::itemChange(change, data);
|
||||
}
|
||||
|
||||
void ButtonRow::updatePolish() {
|
||||
if (m_dirty) {
|
||||
relayout();
|
||||
m_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ButtonRow::invalidate() {
|
||||
m_dirty = true;
|
||||
polish();
|
||||
}
|
||||
|
||||
void ButtonRow::relayout() {
|
||||
const auto allChildren = childItems();
|
||||
|
||||
QList<QQuickItem*> validChildren;
|
||||
for (auto* const child : allChildren) {
|
||||
if (child->isVisible() && !child->inherits("QQuickRepeater"))
|
||||
validChildren.append(child);
|
||||
}
|
||||
const auto nChildren = validChildren.size();
|
||||
const auto totalSpacing = static_cast<qreal>(nChildren - 1) * m_spacing;
|
||||
|
||||
qreal reservedWidth = 0;
|
||||
qreal unreservedWidth = 0;
|
||||
int fillWidthCount = 0;
|
||||
qreal maxHeight = 0;
|
||||
for (auto* const child : validChildren) {
|
||||
maxHeight = qMax(maxHeight, child->implicitHeight());
|
||||
|
||||
const auto prop = child->property("fillWidth");
|
||||
if (!prop.isValid())
|
||||
continue;
|
||||
|
||||
if (prop.toBool()) {
|
||||
fillWidthCount++;
|
||||
unreservedWidth += child->implicitWidth();
|
||||
} else {
|
||||
reservedWidth += child->implicitWidth();
|
||||
}
|
||||
}
|
||||
|
||||
if (fillWidthCount == 0)
|
||||
fillWidthCount = 1; // Avoid divide by 0
|
||||
|
||||
const auto widthPerItem = (width() - totalSpacing - reservedWidth) / static_cast<qreal>(fillWidthCount);
|
||||
|
||||
QList<qreal> baseWidths;
|
||||
baseWidths.reserve(nChildren);
|
||||
for (auto* const child : validChildren)
|
||||
baseWidths.append(child->property("fillWidth").toBool() ? widthPerItem : child->implicitWidth());
|
||||
|
||||
qreal accX = 0;
|
||||
for (int i = 0; i < nChildren; ++i) {
|
||||
auto* const child = validChildren[i];
|
||||
|
||||
// clang-format off
|
||||
auto prevExtraWidth = i > 0 ? getMorphExpansion(validChildren[i - 1]) : 0.0;
|
||||
auto nextExtraWidth = i < nChildren - 1 ? getMorphExpansion(validChildren[i + 1]) : 0.0;
|
||||
// clang-format on
|
||||
|
||||
// Items at edges push by full amount, items in middle push by half
|
||||
if (i > 1)
|
||||
prevExtraWidth /= 2;
|
||||
if (i < nChildren - 2)
|
||||
nextExtraWidth /= 2;
|
||||
|
||||
child->setWidth(baseWidths[i] + getMorphExpansion(child) - prevExtraWidth - nextExtraWidth);
|
||||
child->setHeight(maxHeight);
|
||||
|
||||
child->setX(accX);
|
||||
child->setY(0);
|
||||
accX += child->width() + m_spacing;
|
||||
}
|
||||
|
||||
setImplicitWidth(reservedWidth + unreservedWidth + totalSpacing);
|
||||
setImplicitHeight(maxHeight);
|
||||
}
|
||||
|
||||
qreal ButtonRow::getMorphExpansion(const QQuickItem* item) {
|
||||
return item->property("shapeMorphExpansion").toReal();
|
||||
}
|
||||
|
||||
} // namespace ZShell::components
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <qquickitem.h>
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
class ButtonRow : public QQuickItem {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
|
||||
|
||||
public:
|
||||
explicit ButtonRow(QQuickItem* parent = nullptr);
|
||||
|
||||
[[nodiscard]] qreal spacing() const;
|
||||
void setSpacing(qreal spacing);
|
||||
|
||||
signals:
|
||||
void spacingChanged();
|
||||
|
||||
protected:
|
||||
void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) override;
|
||||
void updatePolish() override;
|
||||
|
||||
private slots:
|
||||
void invalidate();
|
||||
|
||||
private:
|
||||
void relayout();
|
||||
static qreal getMorphExpansion(const QQuickItem* item);
|
||||
|
||||
bool m_dirty;
|
||||
qreal m_spacing;
|
||||
};
|
||||
|
||||
} // namespace ZShell::components
|
||||
Reference in New Issue
Block a user