Skip to content

Commit 9a16756

Browse files
committed
Add on_cleared event
This event is fired when a notification was closed without user interaction, e.g. because the notification timed out (DBus only, and only if supported by the notifications server; undetectable by capabilities), or because the notification was closed by another process (DBus only).
1 parent ff32314 commit 9a16756

File tree

11 files changed

+112
-5
lines changed

11 files changed

+112
-5
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,11 @@ async def main() -> None:
9191
reply_field=ReplyField(
9292
on_replied=lambda text: print("Brutus replied:", text),
9393
),
94+
on_cleared=lambda: print("Notification timed out"),
9495
on_clicked=lambda: print("Notification clicked"),
9596
on_dismissed=lambda: print("Notification dismissed"),
9697
sound=DEFAULT_SOUND,
98+
timeout=10,
9799
)
98100

99101
# Run the event loop forever to respond to user interactions with the notification.

examples/eventloop.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ async def main() -> None:
2727
button_title="Send",
2828
on_replied=lambda text: print("Brutus replied:", text),
2929
),
30+
on_cleared=lambda: print("Notification timed out"),
3031
on_clicked=lambda: print("Notification clicked"),
3132
on_dismissed=lambda: print("Notification dismissed"),
3233
sound=DEFAULT_SOUND,
34+
timeout=10,
3335
)
3436

3537
# Run the event loop forever to respond to user interactions with the notification.

examples/eventloop_handlers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
asyncio.set_event_loop_policy(EventLoopPolicy())
1212

1313

14+
def on_cleared(identifier: str) -> None:
15+
print(f"Notification '{identifier}' was cleared without user interaction")
16+
17+
1418
def on_clicked(identifier: str) -> None:
1519
print(f"Notification '{identifier}' was clicked")
1620

@@ -29,6 +33,7 @@ def on_replied(identifier: str, reply: str) -> None:
2933

3034
async def main() -> None:
3135
notifier = DesktopNotifier(app_name="Sample App")
36+
notifier.on_cleared = on_cleared
3237
notifier.on_clicked = on_clicked
3338
notifier.on_dismissed = on_dismissed
3439
notifier.on_button_pressed = on_button_pressed
@@ -47,6 +52,7 @@ async def main() -> None:
4752
button_title="Send",
4853
),
4954
sound=DEFAULT_SOUND,
55+
timeout=10,
5056
)
5157

5258
# Run the event loop forever to respond to user interactions with the notification.

examples/synchronous.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
button_title="Send",
2424
on_replied=lambda text: print("Brutus replied:", text),
2525
),
26+
on_cleared=lambda: print("Notification timed out"),
2627
on_clicked=lambda: print("Notification clicked"),
2728
on_dismissed=lambda: print("Notification dismissed"),
2829
sound=DEFAULT_SOUND,
30+
timeout=10,
2931
)

src/desktop_notifier/backends/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, app_name: str) -> None:
2828
self.app_name = app_name
2929
self._notification_cache: dict[str, Notification] = dict()
3030

31+
self.on_cleared: Callable[[str], Any] | None = None
3132
self.on_clicked: Callable[[str], Any] | None = None
3233
self.on_dismissed: Callable[[str], Any] | None = None
3334
self.on_button_pressed: Callable[[str, str], Any] | None = None
@@ -142,6 +143,14 @@ async def get_capabilities(self) -> frozenset[Capability]:
142143
"""
143144
...
144145

146+
def handle_cleared(
147+
self, identifier: str, notification: Notification | None = None
148+
) -> None:
149+
if notification and notification.on_cleared:
150+
notification.on_cleared()
151+
elif self.on_cleared:
152+
self.on_cleared(identifier)
153+
145154
def handle_clicked(
146155
self, identifier: str, notification: Notification | None = None
147156
) -> None:

src/desktop_notifier/backends/dbus.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ def _on_closed(self, nid: int, reason: int) -> None:
249249

250250
if reason == NOTIFICATION_CLOSED_DISMISSED:
251251
self.handle_dismissed(identifier, notification)
252+
else:
253+
self.handle_cleared(identifier, notification)
252254

253255
async def get_capabilities(self) -> frozenset[Capability]:
254256
if not self.interface:
@@ -260,6 +262,7 @@ async def get_capabilities(self) -> frozenset[Capability]:
260262
Capability.TITLE,
261263
Capability.TIMEOUT,
262264
Capability.URGENCY,
265+
Capability.ON_CLEARED,
263266
}
264267

265268
# Capabilities supported by some notification servers.

src/desktop_notifier/backends/winrt.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,11 @@ def _on_dismissed(
255255

256256
notification = self._clear_notification_from_cache(sender.tag)
257257

258-
if (
259-
dismissed_args
260-
and dismissed_args.reason == ToastDismissalReason.USER_CANCELED
261-
):
262-
self.handle_dismissed(sender.tag, notification)
258+
if dismissed_args:
259+
# ToastDismissalReason.APPLICATION_HIDDEN and ToastDismissalReason.TIMED_OUT
260+
# both just indicate that the toast was sent to the notifications center
261+
if dismissed_args.reason == ToastDismissalReason.USER_CANCELED:
262+
self.handle_dismissed(sender.tag, notification)
263263

264264
def _on_failed(
265265
self, sender: ToastNotification | None, failed_args: ToastFailedEventArgs | None

src/desktop_notifier/common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ class Notification:
221221
"""Text field shown on an interactive notification. This can be used for example
222222
for messaging apps to reply directly from the notification."""
223223

224+
on_cleared: Callable[[], Any] | None = None
225+
"""Method to call when the notification is cleared without user interaction"""
226+
224227
on_clicked: Callable[[], Any] | None = None
225228
"""Method to call when the notification is clicked"""
226229

@@ -289,6 +292,10 @@ class Capability(Enum):
289292
ATTACHMENT = auto()
290293
"""Supports notification attachments. Allowed file types vary by platform."""
291294

295+
ON_CLEARED = auto()
296+
"""Supports distinguishing between an user closing a notification, and clearing a
297+
notification programmatically, and consequently supports on-cleared callbacks"""
298+
292299
ON_CLICKED = auto()
293300
"""Supports on-clicked callbacks"""
294301

src/desktop_notifier/main.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ async def send(
216216
icon: Icon | None = None,
217217
buttons: Sequence[Button] = (),
218218
reply_field: ReplyField | None = None,
219+
on_cleared: Callable[[], Any] | None = None,
219220
on_clicked: Callable[[], Any] | None = None,
220221
on_dismissed: Callable[[], Any] | None = None,
221222
attachment: Attachment | None = None,
@@ -239,6 +240,7 @@ async def send(
239240
icon=icon,
240241
buttons=tuple(buttons),
241242
reply_field=reply_field,
243+
on_cleared=on_cleared,
242244
on_clicked=on_clicked,
243245
on_dismissed=on_dismissed,
244246
attachment=attachment,
@@ -275,6 +277,23 @@ async def get_capabilities(self) -> frozenset[Capability]:
275277
self._capabilities = await self._backend.get_capabilities()
276278
return self._capabilities
277279

280+
@property
281+
def on_cleared(self) -> Callable[[str], Any] | None:
282+
"""
283+
A method to call when a notification is cleared without user interaction
284+
(e.g. after a timeout, or if cleared by another process)
285+
286+
The method must take the notification identifier as a single argument.
287+
288+
If the notification itself already specifies an on_cleared handler, it will be
289+
used instead of the class-level handler.
290+
"""
291+
return self._backend.on_cleared
292+
293+
@on_cleared.setter
294+
def on_cleared(self, handler: Callable[[str], Any] | None) -> None:
295+
self._backend.on_cleared = handler
296+
278297
@property
279298
def on_clicked(self) -> Callable[[str], Any] | None:
280299
"""

src/desktop_notifier/sync.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def send(
8888
icon: Icon | None = None,
8989
buttons: Sequence[Button] = (),
9090
reply_field: ReplyField | None = None,
91+
on_cleared: Callable[[], Any] | None = None,
9192
on_clicked: Callable[[], Any] | None = None,
9293
on_dismissed: Callable[[], Any] | None = None,
9394
attachment: Attachment | None = None,
@@ -103,6 +104,7 @@ def send(
103104
icon=icon,
104105
buttons=tuple(buttons),
105106
reply_field=reply_field,
107+
on_cleared=on_cleared,
106108
on_clicked=on_clicked,
107109
on_dismissed=on_dismissed,
108110
attachment=attachment,

0 commit comments

Comments
 (0)