How create local notification on MacOS Catalina pyobjc?

拟墨画扇 提交于 2020-06-21 05:08:18

问题


I am having some difficulty finding out how to send local notifications on Catalina using pyobjc.

The closes example I have seen is this: PyObjC "Notifications are not allowed for this application"


回答1:


I have also been searching for this answer, so I'd like to share what I've found:

The first thing you'll notice is that the function notify() defines a class, then returns an instance of it. You might be wonderinf why you can't directly call Notification.send(params). I tried it, but I was getting an error with the PyObjC, which I am unfortunately unable to fix:

# Error
class Notification(NSObject):
objc.BadPrototypeError: Objective-C expects 1 arguments, Python argument has 2 arguments for <unbound selector send of Notification at 0x10e410180>

Now onto the code:

# vscode may show the error: "No name '...' in module 'Foundation'; you can ignore it"
from Foundation import NSUserNotification, NSUserNotificationCenter, NSObject, NSDate
from PyObjCTools import AppHelper


def notify(
        title='Notification',
        subtitle=None, text=None,
        delay=0,

        action_button_title=None,
        action_button_callback=None,

        other_button_title=None,
        other_button_callback=None,

        reply_placeholder=None,
        reply_callback=None
):

  class Notification(NSObject):
    def send(self):
      notif = NSUserNotification.alloc().init()

      if title is not None:
        notif.setTitle_(title)
      if subtitle is not None:
        notif.setSubtitle_(subtitle)
      if text is not None:
        notif.setInformativeText_(text)

      # notification buttons (main action button and other button)
      if action_button_title:
        notif.setActionButtonTitle_(action_button_title)
        notif.set_showsButtons_(True)

      if other_button_title:
        notif.setOtherButtonTitle_(other_button_title)
        notif.set_showsButtons_(True)

      # reply button
      if reply_callback:
        notif.setHasReplyButton_(True)
        if reply_placeholder:
          notif.setResponsePlaceholder_(reply_placeholder)

      NSUserNotificationCenter.defaultUserNotificationCenter().setDelegate_(self)

      # setting delivery date as current date + delay (in seconds)
      notif.setDeliveryDate_(NSDate.dateWithTimeInterval_sinceDate_(delay, NSDate.date()))

      # schedule the notification send
      NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notif)

      # on if any of the callbacks are provided, start the event loop (this will keep the program from stopping)
      if action_button_callback or other_button_callback or reply_callback:
        print('started')
        AppHelper.runConsoleEventLoop()

    def userNotificationCenter_didDeliverNotification_(self, center, notif):
      print('delivered notification')

    def userNotificationCenter_didActivateNotification_(self, center, notif):
      print('did activate')
      response = notif.response()

      if notif.activationType() == 1:
        # user clicked on the notification (not on a button)
        # don't stop event loop because the other buttons can still be pressed
        pass

      elif notif.activationType() == 2:
        # user clicked on the action button
        action_button_callback()
        AppHelper.stopEventLoop()

      elif notif.activationType() == 3:
        # user clicked on the reply button
        reply_text = response.string()
        reply_callback(reply_text)
        AppHelper.stopEventLoop()

  # create the new notification
  new_notif = Notification.alloc().init()

  # return notification
  return new_notif


def main():
  n = notify(
      title='Notification',
      delay=0,
      action_button_title='Action',
      action_button_callback=lambda: print('Action'),
      # other_button_title='Other',
      # other_button_callback=lambda: print('Other'),

      reply_placeholder='Enter your reply please',
      reply_callback=lambda reply: print('Replied: ', reply),
  )
  n.send()


if __name__ == '__main__':
  main()

Explanation

The notify() function takes in quite a few parameters (they are self-explanatory). The delay is how many seconds later the notification will appear. Note that if you set a delay that's longer than the execution of the program, the notification will be sent ever after the program is being executed.

You'll see the button parameters. There are three types of buttons:

  1. Action button: the dominant action
  2. Other button: the secondary action
  3. Reply button: the button that opens a text field and takes a user input. This is commonly seen in messaging apps like iMessage.

All those if statements are setting the buttons appropriately and self explanatory. For instance, if the parameters for the other button are not provided, a Other button will not be shown.

One thing you'll notice is that if there are buttons, we are starting the console event loop:

      if action_button_callback or other_button_callback or reply_callback:
        print('started')
        AppHelper.runConsoleEventLoop()

This is a part of Python Objective-C. This is not a good explanation, but it basically keeps program "on" (I hope someone cane give a better explanation).

Basically, if you specify that you want a button, the program will continue to be "on" until AppHelper.stopEventLoop() (more about this later).

Now there are some "hook" functions:

  1. userNotificationCenter_didDeliverNotification_(self, notification_center, notification): called when the notification is delivered
  2. userNotificationCenter_didActivateNotification_(self, notification_center, notification): called when the user interacts with the notification (clicks, clicks action button, or reply) (documentation)

There surely are more, but I do not think there is a hook for the notification being dismissed or ignored, unfortunately.

With userNotificationCenter_didActivateNotification_, we can define some callbacks:


    def userNotificationCenter_didActivateNotification_(self, center, notif):
      print('did activate')
      response = notif.response()

      if notif.activationType() == 1:
        # user clicked on the notification (not on a button)
        # don't stop event loop because the other buttons can still be pressed
        pass

      elif notif.activationType() == 2:
        # user clicked on the action button

        # action button callback
        action_button_callback()
        AppHelper.stopEventLoop()

      elif notif.activationType() == 3:
        # user clicked on the reply button
        reply_text = response.string()

        # reply button callback
        reply_callback(reply_text)
        AppHelper.stopEventLoop()

There are different activation types for the types of actions. The text from the reply action can also be retrieved as shown.

You'll also notice the AppHelper.stopEventLoop() at the end. This means to "end" the program from executing, since the notification has been dealt with by the user.

Now let's address all the problems with this solution.

Problems

  1. The program will never stop if the user does not interact with the notification. The notification will slide away into the notification center and may or may never be interacted with. As I stated before, there's no hook for notification ignored or notification dismissed, so we cannot call AppHelper.stopEventLoop() at times like this.
  2. Because AppHelper.stopEventLoop() is being run after interaction, it is not possible to send multiple notifications with callbacks, as the program will stop executing after the first notification is interacted with.
  3. Although I can show the Other button (and give it text), I couldn't find a way to give it a callback. This is why I haven't addressed it in the above code block. I can give it text, but it's essentially a dummy button as it cannot do anything.

Should I still use this solution?

If you want notifications with callbacks, you probably should not, because of the problems I addressed.

If you only want to show notifications to alert the user on something, yes.

Other solutions

PYNC is a wrapper around terminal-notifier. However, both received their last commit in 2018. Alerter seems to be a successor to terminal-notifier, but there is not Python wrapper.

You can also try running applescript to send notifications, but you cannot set callbacks, nor can you change the icon.

I hope this answer has helped you. I am also trying to find out how to reliably send notifications with callbacks on Mac OS. I've figured out how to send notifications, but callbacks is the issue.



来源:https://stackoverflow.com/questions/62234033/how-create-local-notification-on-macos-catalina-pyobjc

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!