-
Notifications
You must be signed in to change notification settings - Fork 4
Skip multiprocessing if no callback actions requested; Add sound param; Fix signal handling #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
michelcrypt4d4mus
wants to merge
24
commits into
Jorricks:main
Choose a base branch
from
michelcrypt4d4mus:add_sound_param_dealloc_class_better
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
1d29cd5
MACOS_NOTIFICATIONS_AS_DAEMON constant; don't instantiate SimpleQueue…
5b2ed58
undo unnecessary changes
9a1033b
Don't register SIGINT handler when MACOS_NOTIFICATIONS_AS_DAEMON is true
ccb0b19
sound param
3246df9
Fix
a840d67
Merge branch 'main' into add_sound_param
5a1cbc2
soundName
cb39233
docs
f52f01b
Refactor out build_notification
064c642
Move classes
932dfc7
Reorg
428b569
comment
5b33a2e
remove autorelease_pool block comment
3140464
No need to return MacOSNotification object
cfb1101
comment
1278caa
comment
4edaaf1
comment
0127503
allow_multiprocessing arg
3254c0f
Allow SIGINT
fdda71c
remove env var
2cb0319
comments
55654ae
unused import
ae00c08
Put back the original signal handler before SIGINT raise
8d906cb
Remove NotificationProcess altogether
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,114 +1,143 @@ | ||
""" | ||
This module is responsible for creating the notifications in the C-layer and listening/reporting about user activity. | ||
""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
import re | ||
from multiprocessing import SimpleQueue | ||
from typing import Any | ||
from typing import Any, Type | ||
|
||
import ctypes | ||
from AppKit import NSImage | ||
from Foundation import NSDate, NSObject, NSURL, NSUserNotification, NSUserNotificationCenter | ||
from objc import python_method | ||
from PyObjCTools import AppHelper | ||
|
||
from mac_notifications.notification_config import JSONNotificationConfig | ||
|
||
logger = logging.getLogger() | ||
|
||
""" | ||
This module is responsible for creating the notifications in the C-layer and listening/reporting about user activity. | ||
""" | ||
|
||
def create_notification(config: JSONNotificationConfig, queue_to_submit_events_to: SimpleQueue | None) -> Any: | ||
def create_notification(config: JSONNotificationConfig, queue_to_submit_events_to: SimpleQueue | None) -> None: | ||
""" | ||
Create a notification and possibly listed & report about notification activity. | ||
Create a notification and possibly listen & report about notification activity. | ||
:param config: The configuration of the notification to send. | ||
:param queue_to_submit_events_to: The Queue to submit user activity related to the callbacks to. If this argument | ||
is passed, it will start the event listener after it created the Notifications. If this is None, it will only | ||
create the notification. | ||
""" | ||
|
||
class MacOSNotification(NSObject): | ||
def send(self): | ||
"""Sending of the notification""" | ||
notification = NSUserNotification.alloc().init() | ||
notification.setIdentifier_(config.uid) | ||
if config is not None: | ||
notification.setTitle_(config.title) | ||
if config.subtitle is not None: | ||
notification.setSubtitle_(config.subtitle) | ||
if config.text is not None: | ||
notification.setInformativeText_(config.text) | ||
if config.icon is not None: | ||
url = NSURL.alloc().initWithString_(f"file://{config.icon}") | ||
image = NSImage.alloc().initWithContentsOfURL_(url) | ||
notification.setContentImage_(image) | ||
|
||
# Notification buttons (main action button and other button) | ||
if config.action_button_str: | ||
notification.setActionButtonTitle_(config.action_button_str) | ||
notification.setHasActionButton_(True) | ||
|
||
if config.snooze_button_str: | ||
notification.setOtherButtonTitle_(config.snooze_button_str) | ||
|
||
if config.reply_callback_present: | ||
notification.setHasReplyButton_(True) | ||
if config.reply_button_str: | ||
notification.setResponsePlaceholder_(config.reply_button_str) | ||
|
||
NSUserNotificationCenter.defaultUserNotificationCenter().setDelegate_(self) | ||
|
||
# Setting delivery date as current date + delay (in seconds) | ||
notification.setDeliveryDate_( | ||
NSDate.dateWithTimeInterval_sinceDate_(config.delay_in_seconds, NSDate.date()) | ||
) | ||
|
||
# Schedule the notification send | ||
NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notification) | ||
|
||
# Wait for the notification CallBack to happen. | ||
if queue_to_submit_events_to: | ||
logger.debug("Started listening for user interactions with notifications.") | ||
AppHelper.runConsoleEventLoop() | ||
|
||
def userNotificationCenter_didDeliverNotification_( | ||
self, center: "_NSConcreteUserNotificationCenter", notif: "_NSConcreteUserNotification" # type: ignore # noqa | ||
) -> None: | ||
"""Respond to the delivering of the notification.""" | ||
logger.debug(f"Delivered: {notif.identifier()}") | ||
|
||
def userNotificationCenter_didActivateNotification_( | ||
self, center: "_NSConcreteUserNotificationCenter", notif: "_NSConcreteUserNotification" # type: ignore # noqa | ||
) -> None: | ||
""" | ||
Respond to a user interaction with the notification. | ||
""" | ||
identifier = notif.identifier() | ||
response = notif.response() | ||
activation_type = notif.activationType() | ||
|
||
if queue_to_submit_events_to is None: | ||
raise ValueError("Queue should not be None here.") | ||
else: | ||
queue: SimpleQueue = queue_to_submit_events_to | ||
|
||
logger.debug(f"User interacted with {identifier} with activationType {activation_type}.") | ||
if activation_type == 1: | ||
# user clicked on the notification (not on a button) | ||
pass | ||
|
||
elif activation_type == 2: # user clicked on the action button | ||
queue.put((identifier, "action_button_clicked", "")) | ||
|
||
elif activation_type == 3: # User clicked on the reply button | ||
queue.put((identifier, "reply_button_clicked", response.string())) | ||
|
||
# create the new notification | ||
new_notif = MacOSNotification.alloc().init() | ||
|
||
# return notification | ||
return new_notif | ||
notification = _build_notification(config) | ||
macos_notification = MacOSNotification.alloc().init() | ||
macos_notification.send(notification, config, queue_to_submit_events_to) | ||
|
||
|
||
class MacOSNotification(NSObject): | ||
@python_method | ||
def send( | ||
self, | ||
notification: NSUserNotification, | ||
config: JSONNotificationConfig, | ||
queue_to_submit_events_to: SimpleQueue | None | ||
): | ||
"""Sending of the notification""" | ||
self.queue_to_submit_events_to = queue_to_submit_events_to | ||
NSUserNotificationCenter.defaultUserNotificationCenter().setDelegate_(self) | ||
|
||
# Setting delivery date as current date + delay (in seconds) | ||
notification.setDeliveryDate_( | ||
NSDate.dateWithTimeInterval_sinceDate_(config.delay_in_seconds, NSDate.date()) | ||
) | ||
|
||
# Schedule the notification send | ||
NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notification) | ||
|
||
# Wait for the notification CallBack to happen. | ||
if queue_to_submit_events_to: | ||
logger.debug("Started listening for user interactions with notifications.") | ||
AppHelper.runConsoleEventLoop() | ||
|
||
def userNotificationCenter_didDeliverNotification_( | ||
self, | ||
center: "_NSConcreteUserNotificationCenter", | ||
notif: "_NSConcreteUserNotification" | ||
) -> None: | ||
"""Respond to the delivering of the notification.""" | ||
logger.debug(f"Delivered: {notif.identifier()}") | ||
|
||
def userNotificationCenter_didActivateNotification_( | ||
self, | ||
center: "_NSConcreteUserNotificationCenter", | ||
notif: "_NSConcreteUserNotification" # type: ignore # noqa | ||
) -> None: | ||
""" | ||
Respond to a user interaction with the notification. | ||
""" | ||
identifier = notif.identifier() | ||
response = notif.response() | ||
activation_type = notif.activationType() | ||
|
||
if self.queue_to_submit_events_to is None: | ||
raise ValueError("Queue should not be None here.") | ||
else: | ||
queue: SimpleQueue = self.queue_to_submit_events_to | ||
|
||
logger.debug(f"User interacted with {identifier} with activationType {activation_type}.") | ||
if activation_type == 1: | ||
# user clicked on the notification (not on a button) | ||
pass | ||
|
||
elif activation_type == 2: # user clicked on the action button | ||
queue.put((identifier, "action_button_clicked", "")) | ||
|
||
elif activation_type == 3: # User clicked on the reply button | ||
queue.put((identifier, "reply_button_clicked", response.string())) | ||
|
||
|
||
def cancel_notification(uid:str) -> None: | ||
notification = NSUserNotification.alloc().init() | ||
notification.setIdentifier_(uid) | ||
NSUserNotificationCenter.defaultUserNotificationCenter().removeDeliveredNotification_(notification) | ||
|
||
|
||
def _build_notification(config: JSONNotificationConfig) -> NSUserNotification: | ||
notification = NSUserNotification.alloc().init() | ||
notification.setIdentifier_(config.uid) | ||
if config is not None: | ||
notification.setTitle_(config.title) | ||
if config.subtitle is not None: | ||
notification.setSubtitle_(config.subtitle) | ||
if config.text is not None: | ||
notification.setInformativeText_(config.text) | ||
if config.sound is not None: | ||
notification.setSoundName_(config.sound) | ||
if config.icon is not None: | ||
url = NSURL.alloc().initWithString_(f"file://{config.icon}") | ||
image = NSImage.alloc().initWithContentsOfURL_(url) | ||
notification.setContentImage_(image) | ||
|
||
# Notification buttons (main action button and other button) | ||
if config.action_button_str: | ||
notification.setActionButtonTitle_(config.action_button_str) | ||
notification.setHasActionButton_(True) | ||
|
||
if config.snooze_button_str: | ||
notification.setOtherButtonTitle_(config.snooze_button_str) | ||
|
||
if config.reply_callback_present: | ||
notification.setHasReplyButton_(True) | ||
if config.reply_button_str: | ||
notification.setResponsePlaceholder_(config.reply_button_str) | ||
|
||
return notification | ||
|
||
|
||
# Hardcore way to dealloc an Objective-C class from https://github.com/albertz/chromehacking/blob/master/disposeClass.py | ||
def dispose_of_objc_class(cls: Type): | ||
"""Deallocate an objective C class ('del cls' does not remove the class from memory).""" | ||
address = int(re.search("0x[0-9a-f]+", repr(cls)).group(0), 16) | ||
logger.info(f"Disposing of class '{cls.__name__}' at addr: {hex(address)}") | ||
print(f"Disposing of class '{cls.__name__}' at addr: {hex(address)}") | ||
ctypes.pythonapi.objc_disposeClassPair.restype = None | ||
ctypes.pythonapi.objc_disposeClassPair.argtypes = (ctypes.c_void_p,) | ||
ctypes.pythonapi.objc_disposeClassPair(address) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Jorricks why do you need both
atexit()
and customSIGINT
handling that do the exact same thing (callself.cleanup()
)? Seems like justatexit()
should be sufficient to accomplish what you are going for?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly do not recall.
I think we can indeed remove the
catch_keyboard_interrupt
but let's save that for another MR.