All four Print binds go through hypr/scripts/shot.sh. Every capture lands on the clipboard and posts a notification showing the picture, with Open, Save (for clipboard captures, which wait in the runtime dir) and Edit when satty is installed. Cancelling the region picker does nothing. Notification cards now load images passed as a path hint: Quickshell routes those through the icon provider, which returned a placeholder.
283 lines
9.7 KiB
QML
283 lines
9.7 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import Quickshell.Services.Notifications
|
|
import "root:/config"
|
|
import "root:/components"
|
|
import "root:/services"
|
|
|
|
// One notification, used both in the history list and in the toast drawer.
|
|
//
|
|
// Laid out like an entry in NieR's message log rather than as a card: a dot
|
|
// and the sender's name running into a hairline, the summary, the body, and
|
|
// pill buttons for any actions. There is no panel of its own — both places it
|
|
// appears are already hull surfaces — so rest is bare, hover lifts the entry
|
|
// onto a soft rounded wash, and entries are divided by the hairline in their
|
|
// own header.
|
|
//
|
|
// Urgency is the dot: the light for normal, grey for low, red for critical.
|
|
// A critical entry also takes the red into its sender line, and nothing else.
|
|
Item {
|
|
id: root
|
|
|
|
required property var notif
|
|
property bool showActions: true
|
|
|
|
readonly property bool critical: root.notif?.urgency === NotificationUrgency.Critical
|
|
readonly property color tone: root.notif
|
|
? Notifs.urgencyColor(root.notif.urgency)
|
|
: Theme.primary
|
|
|
|
// Kept for callers that size things to the card's edges; there is no lean
|
|
// or chop under the YoRHa theme.
|
|
readonly property real lean: 0
|
|
readonly property real chop: 0
|
|
|
|
readonly property bool hovered: hover.hovered
|
|
|
|
signal dismissed()
|
|
|
|
implicitHeight: layout.implicitHeight + Theme.padM * 2
|
|
|
|
// Hover wash. Inset by padS and rounded by radiusL, which is the frame's
|
|
// radius less that inset: an entry in the bottom corner of the drawer gets
|
|
// a corner concentric with the drawer's own, instead of a square one that
|
|
// ran out past the drawer's curve.
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.padS / 2
|
|
radius: Theme.radiusL
|
|
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durFast }
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: layout
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Theme.padM
|
|
spacing: 4
|
|
|
|
// Sender: marker, glyph, name, then the hairline to the edge.
|
|
Item {
|
|
width: parent.width
|
|
height: 16
|
|
|
|
Rectangle {
|
|
id: marker
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: Theme.marker
|
|
height: Theme.marker
|
|
radius: width / 2
|
|
color: root.tone
|
|
}
|
|
|
|
Icon {
|
|
id: glyph
|
|
visible: text !== ""
|
|
anchors.left: marker.right
|
|
anchors.leftMargin: Theme.padS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: Notifs.appIconFor(root.notif)
|
|
color: root.critical ? Theme.alarm : Theme.muted
|
|
font.pixelSize: Theme.fsBody
|
|
}
|
|
|
|
Text {
|
|
id: sender
|
|
anchors.left: glyph.visible ? glyph.right : marker.right
|
|
anchors.leftMargin: Theme.padS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: (root.notif?.appName || "System").toUpperCase()
|
|
color: root.critical ? Theme.alarm : Theme.muted
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fsMicro
|
|
font.letterSpacing: 2
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.left: sender.right
|
|
anchors.leftMargin: Theme.padS
|
|
anchors.right: close.left
|
|
anchors.rightMargin: Theme.padS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: 1
|
|
color: Theme.alpha(Theme.outline, 0.5)
|
|
}
|
|
|
|
// Close affordance: always there, quiet until the row is hovered.
|
|
Icon {
|
|
id: close
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: ""
|
|
opacity: root.hovered ? 1 : 0.0
|
|
color: closeHover.hovered ? Theme.accent : Theme.muted
|
|
font.pixelSize: Theme.fsBody
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Theme.durFast }
|
|
}
|
|
|
|
HoverHandler {
|
|
id: closeHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: root.dismissed()
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
leftPadding: Theme.marker + Theme.padS
|
|
text: root.notif?.summary || ""
|
|
color: root.hovered ? Theme.accent : Theme.text
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 2
|
|
wrapMode: Text.WordWrap
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsBody
|
|
font.weight: Theme.weightDisplay
|
|
renderType: Text.NativeRendering
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durFast }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
leftPadding: Theme.marker + Theme.padS
|
|
visible: text !== ""
|
|
text: root.notif?.body || ""
|
|
color: Theme.subtext
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 4
|
|
wrapMode: Text.WordWrap
|
|
textFormat: Text.StyledText
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsSmall
|
|
lineHeight: 1.1
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
// Inline image, when the sender supplied one, rounded in a hairline frame.
|
|
ClippingRectangle {
|
|
x: Theme.marker + Theme.padS
|
|
visible: inline.source != "" && inline.status === Image.Ready
|
|
width: inline.width + 2
|
|
height: inline.height + 2
|
|
radius: Theme.radiusM
|
|
color: "transparent"
|
|
border.width: 1
|
|
border.color: Theme.overlay
|
|
|
|
Image {
|
|
id: inline
|
|
x: 1
|
|
y: 1
|
|
// A path given as an image hint (notify-send -h image-path:…)
|
|
// comes back routed through the icon provider, which treats
|
|
// it as an icon name and returns a placeholder. Load the file.
|
|
source: {
|
|
const img = root.notif?.image || "";
|
|
if (img.startsWith("image://icon//")) return "file://" + img.slice("image://icon/".length);
|
|
return img.startsWith("/") ? "file://" + img : img;
|
|
}
|
|
sourceSize.height: 90
|
|
fillMode: Image.PreserveAspectFit
|
|
asynchronous: true
|
|
}
|
|
}
|
|
|
|
Row {
|
|
x: Theme.marker + Theme.padS
|
|
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
|
|
spacing: Theme.padS
|
|
topPadding: 4
|
|
|
|
Repeater {
|
|
model: root.notif?.actions ?? []
|
|
|
|
// A hairline pill. Hover lifts and lights it; it never inverts.
|
|
Item {
|
|
id: actionChip
|
|
required property var modelData
|
|
|
|
implicitWidth: chipRow.implicitWidth + Theme.padM * 2
|
|
implicitHeight: 24
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: height / 2
|
|
color: actionHover.hovered ? Theme.treatmentHoverFill : "transparent"
|
|
border.width: 1
|
|
border.color: actionHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durFast }
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: chipRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.padS
|
|
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 4
|
|
height: 4
|
|
radius: 2
|
|
color: actionLabel.color
|
|
opacity: actionHover.hovered ? 1 : 0.4
|
|
}
|
|
|
|
Text {
|
|
id: actionLabel
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: actionChip.modelData.text.toUpperCase()
|
|
color: actionHover.hovered ? Theme.accent : Theme.text
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsMicro
|
|
font.letterSpacing: Theme.tracking
|
|
renderType: Text.NativeRendering
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: actionHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: {
|
|
actionChip.modelData.invoke();
|
|
root.dismissed();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: hover
|
|
}
|
|
|
|
// Middle click dismisses, matching the old dunst muscle memory.
|
|
TapHandler {
|
|
acceptedButtons: Qt.MiddleButton
|
|
onTapped: root.dismissed()
|
|
}
|
|
}
|