The Problem
Desktop notifications appear and disappear. Users miss alerts when they're away from their keyboard. A notification center stores notification history, groups by application, and allows users to review missed notifications at their convenience.
The Naive Approach
Show notifications as ephemeral popups that disappear after 5 seconds:
Timer { interval: 5000; onTriggered: popup.visible = false }No history, no grouping, no way to find a notification the user missed while grabbing coffee.
Think of the notification center as your email inbox. Notifications (emails) arrive throughout the day. Some are important (critical urgency), some are spam (low urgency). The inbox groups by sender (app name), marks unread, and lets you review or dismiss each one.
The Idea
Create a PopupWindow that opens when clicking a bell icon in the top bar. It shows a ListView of all notifications received during the session, grouped by application, with clear-all and dismiss actions.
Let's Build It
// NotificationCenter.qml
PopupWindow {
id: center
width: 360; height: 480
anchor.window: topBar
anchor.rect.y: parentWindow.height
visible: false
dismissOnClickOutside: true
Rectangle {
anchors.fill: parent; radius: 8; color: "#1e1e2e"
Column {
anchors { fill: parent; margins: 12 }; spacing: 8
// Header
Row {
width: parent.width; spacing: 8
Text { text: "Notifications"; font.pixelSize: 14; font.bold: true; color: "#cdd6f4"; anchors.verticalCenter: parent.verticalCenter }
Item { width: parent.width - 160; height: 1 }
Text { text: notifications.count + " new"; font.pixelSize: 11; color: "#6c7086"; anchors.verticalCenter: parent.verticalCenter }
Rectangle { width: 60; height: 24; radius: 4; color: "#313244"
Text { anchors.centerIn: parent; text: "Clear All"; color: "#f38ba8"; font.pixelSize: 11 }
MouseArea { anchors.fill: parent; onClicked: center.clearAll() }
}
}
// Notification list
ListView {
id: notifList
width: parent.width; height: parent.height - 50
model: notifications
spacing: 4; clip: true
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width; height: 70; radius: 6; color: "#313244"
Column {
anchors { fill: parent; margins: 10 }; spacing: 4
Row {
spacing: 6
Rectangle {
width: 6; height: 6; radius: 3
color: modelData.urgency === 2 ? "#f38ba8" : modelData.urgency === 1 ? "#fab387" : "#6c7086"
anchors.verticalCenter: parent.verticalCenter
}
Text { text: modelData.appName || "System"; font.pixelSize: 11; color: "#89b4fa" }
Item { width: parent.width - 150; height: 1 }
Text { text: formatTime(modelData.timestamp); font.pixelSize: 10; color: "#6c7086" }
}
Text { text: modelData.summary || ""; font.pixelSize: 13; font.bold: true; color: "#cdd6f4"; elide: Text.ElideRight }
Text { text: modelData.body || ""; font.pixelSize: 11; color: "#a6adc8"; elide: Text.ElideRight; maximumLineCount: 2; wrapMode: Text.WordWrap }
// Action buttons
Row {
spacing: 6; visible: modelData.actions && modelData.actions.length > 0
Repeater {
model: modelData.actions || []
delegate: Rectangle {
required property string modelData
width: Math.min(80, modelData.length * 8 + 16); height: 20; radius: 3; color: "#45475a"
Text { anchors.centerIn: parent; text: modelData; color: "#cdd6f4"; font.pixelSize: 10 }
MouseArea { anchors.fill: parent; onClicked: Qt.dbusCall("org.freedesktop.Notifications", "InvokeAction", modelData.id, modelData) }
}
}
}
}
MouseArea {
anchors.fill: parent
onClicked: { notifications.remove(index); center.dismissed(modelData.id) }
}
}
ScrollBar.vertical: ScrollBar { active: true }
}
}
}
property var notifications: ListModel {}
signal dismissed(int id)
signal clearAll()
function formatTime(date) {
if (!date) return ""
return date.toLocaleTimeString(Qt.locale(), Locale.ShortFormat)
}
function addNotification(n) {
notifications.insert(0, n)
// Keep max 100
while (notifications.count > 100) notifications.remove(notifications.count - 1)
}
}The notification center stores notifications in a ListModel. Each notification shows app name, urgency dot, summary, body, optional action buttons, and timestamp. "Clear All" dismisses everything. Clicking a notification removes it individually.
Let's Improve It
Add notification grouping by application:
function addNotification(n) {
// Group by app — find existing group or create new
for (var i = 0; i < notifications.count; i++) {
var g = notifications.get(i)
if (g.appName === n.appName && g.isGroup) {
g.count++
g.lastNotification = n
notifications.set(i, g)
return
}
}
notifications.insert(0, { isGroup: true, appName: n.appName, count: 1, lastNotification: n })
}No limit on history. Storing thousands of notifications eventually consumes all available memory. Cap at 100-200 entries. Persist to disk if you need permanent history.
Forgetting urgency colors. Critical notifications should be visually distinct from low-priority ones. Use color-coded urgency dots and maybe a different background tint.
Not handling action callbacks. Notification actions (Reply, Mark Read, Open) require D-Bus calls back to the sending application. Parse modelData.actions and call InvokeAction with the correct ID.
Under the Hood
The notification center receives notifications from the notification daemon (which implements org.freedesktop.Notifications on D-Bus). Each notification has a unique ID, app name, summary, body, urgency level (0=low, 1=normal, 2=critical), and optional action buttons. The center stores them for the session duration.
Exercises
- Notification center stores session notifications in a ListModel
- Group by application for cleaner display
- Show urgency level with color-coded dots
- Cap history at 100-200 entries to manage memory