Build the Panama Hyprland desktop
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
// One notification, rendered GNOME-style: app icon, app name, summary, body,
|
||||
// optional image, action buttons and an inline reply box.
|
||||
//
|
||||
// Shared by the toasts and the notification centre so a notification looks the
|
||||
// same wherever you meet it.
|
||||
//
|
||||
// IconButton/ThemedIcon come from the quicksettings module — they belong in
|
||||
// widgets/, but that directory is owned elsewhere.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.quicksettings
|
||||
import qs.widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var notification
|
||||
|
||||
// The centre renders a denser card than a toast banner.
|
||||
property bool compact: false
|
||||
|
||||
signal dismissed
|
||||
readonly property alias hovered: hover.containsMouse
|
||||
// Raised when the reply field takes focus: the toast window has to ask the
|
||||
// compositor for keyboard focus before anything can be typed.
|
||||
signal replyFocusChanged(bool focused)
|
||||
|
||||
// The image sits outside `layout`, so the card has to be tall enough for
|
||||
// whichever of the two is bigger.
|
||||
implicitHeight: Math.max(layout.implicitHeight + 24, image.visible ? 96 : 0)
|
||||
radius: Theme.cardRadius
|
||||
border.width: 0
|
||||
color: hover.containsMouse ? Theme.alpha(Theme.fg, 0.1) : Theme.alpha(Theme.fg, 0.06)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
|
||||
// Critical notifications get a red edge rather than a colour wash, so the
|
||||
// text contrast never changes.
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 6
|
||||
width: 3
|
||||
radius: 1.5
|
||||
border.width: 0
|
||||
color: Theme.urgent
|
||||
visible: root.notification.urgency === NotificationUrgency.Critical
|
||||
}
|
||||
|
||||
readonly property var defaultAction: {
|
||||
for (const action of root.notification.actions) {
|
||||
if (action.identifier === "default")
|
||||
return action;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// "default" is the card click, so it never becomes a button.
|
||||
readonly property bool hasButtons: {
|
||||
for (const action of root.notification.actions) {
|
||||
if (action.identifier !== "default")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Everything except the buttons: clicking the body runs the notification's
|
||||
// default action, which is what GNOME does.
|
||||
MouseArea {
|
||||
id: hover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.defaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (root.defaultAction)
|
||||
root.defaultAction.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 13
|
||||
implicitSize: 22
|
||||
asynchronous: true
|
||||
source: {
|
||||
if (root.notification.appIcon !== "")
|
||||
return Quickshell.iconPath(root.notification.appIcon, true);
|
||||
const entry = DesktopEntries.byId(root.notification.desktopEntry);
|
||||
return entry ? Quickshell.iconPath(entry.icon, true) : "";
|
||||
}
|
||||
visible: source !== ""
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 6
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 6
|
||||
size: 24
|
||||
iconSize: 12
|
||||
tint: Theme.fgDim
|
||||
icon: "window-close-symbolic"
|
||||
onClicked: root.dismissed()
|
||||
}
|
||||
|
||||
Column {
|
||||
id: layout
|
||||
anchors.left: appIcon.visible ? appIcon.right : parent.left
|
||||
anchors.leftMargin: appIcon.visible ? 10 : 14
|
||||
anchors.right: image.visible ? image.left : closeButton.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 12
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: {
|
||||
const app = root.notification.appName || "Notification";
|
||||
const time = Notifs.timeText(root.notification.id);
|
||||
return time === "" ? app : app + " · " + time;
|
||||
}
|
||||
color: Theme.fgMuted
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
text: root.notification.summary
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
text: root.notification.body
|
||||
color: Theme.fgDim
|
||||
// The server advertises bodyMarkupSupported, so bodies arrive with
|
||||
// the freedesktop subset of HTML in them.
|
||||
textFormat: Text.StyledText
|
||||
linkColor: Theme.accent
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: root.compact ? 6 : 3
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 4
|
||||
visible: actionRow.visible || replyBox.visible
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: actionRow
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
visible: root.hasButtons
|
||||
|
||||
Repeater {
|
||||
model: root.notification.actions
|
||||
|
||||
Rectangle {
|
||||
id: actionButton
|
||||
|
||||
required property var modelData
|
||||
|
||||
visible: actionButton.modelData.identifier !== "default"
|
||||
width: visible ? actionLabel.implicitWidth + 22 : 0
|
||||
height: visible ? 28 : 0
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
color: actionMouse.containsMouse ? Theme.alpha(Theme.accent, 0.28) : Theme.alpha(Theme.fg, 0.1)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: actionLabel
|
||||
anchors.centerIn: parent
|
||||
text: actionButton.modelData.text
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: actionButton.modelData.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: replyBox
|
||||
width: parent.width
|
||||
height: 36
|
||||
visible: root.notification.hasInlineReply
|
||||
radius: Theme.cardRadius - 2
|
||||
border.width: 1
|
||||
border.color: replyInput.activeFocus ? Theme.alpha(Theme.accent, 0.5) : Theme.alpha(Theme.fg, 0.12)
|
||||
color: Theme.alpha(Theme.fg, 0.08)
|
||||
|
||||
TextInput {
|
||||
id: replyInput
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: replySend.left
|
||||
anchors.rightMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Theme.fg
|
||||
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
||||
selectedTextColor: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
clip: true
|
||||
|
||||
onActiveFocusChanged: root.replyFocusChanged(replyInput.activeFocus)
|
||||
|
||||
onAccepted: {
|
||||
if (replyInput.text.length === 0)
|
||||
return;
|
||||
root.notification.sendInlineReply(replyInput.text);
|
||||
replyInput.text = "";
|
||||
root.dismissed();
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: replyInput.text === ""
|
||||
text: root.notification.inlineReplyPlaceholder || "Reply"
|
||||
color: Theme.fgMuted
|
||||
font: replyInput.font
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: replySend
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 28
|
||||
iconSize: 14
|
||||
icon: "go-next-symbolic"
|
||||
onClicked: replyInput.accepted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification images (album art, avatars, screenshots) sit on the right.
|
||||
ClippingRectangle {
|
||||
id: image
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 14
|
||||
anchors.top: closeButton.bottom
|
||||
anchors.topMargin: 4
|
||||
implicitWidth: 52
|
||||
implicitHeight: 52
|
||||
radius: 8
|
||||
border.width: 0
|
||||
color: "transparent"
|
||||
visible: root.notification.image !== ""
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.notification.image
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
sourceSize.width: 104
|
||||
sourceSize.height: 104
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user