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"
Edit (June 27, 2020): I've created a package which has functionality to display notifications on Mac OS here. It will use PyObjC to create and display notifications. If It does not work for whatever reason, it will fallback to AppleScript notifications with osascript. I did some testing and found that the PyObjC notifications work on some devices but don't on some.
Answer:
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 wondering 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:
Action button: the dominant action
Other button: the secondary action
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:
userNotificationCenter_didDeliverNotification_(self, notification_center, notification): called when the notification is delivered
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
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.
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.
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.
Related
I want this command to work in different places at the same time. When I run it on one channel, and my friend runs it on another channel, the command starts to be duplicated when one of us presses the button. I don't click anything, but if my friend clicks on the button in his channel, the message will be sent in both channels.
import random
import string
import discord
from discord_components import DiscordComponents, Button, ButtonStyle
#bot.command()
async def random_screenshot(ctx):
while True:
letters = string.ascii_lowercase + string.digits
rand_string = ''.join(random.choice(letters) for i in range(6))
link = "https://prnt.sc/" + str(rand_string)
await ctx.send(link,
components=[
Button(style=ButtonStyle.blue, label="Next", emoji="➡")])
await bot.wait_for('button_click')
This usually happens with all commands when i use the while loop
The while loop isn't the problem here (though it's a separate problem).
What's happening is that await bot.wait_for("button_click") doesn't care what button is pressed. This means that when the command is run twice, then a button is clicked, both messages respond.
You'll want to make sure that the wait_for only continues if the button is our message's button (instead of another message's button). To do that, you'll need to do two things.
First, we need to generate a random string and set our button's custom id to it. This is so that we can know which button was pressed depending on its custom id. But wait, the discord_components library already generates an ID for us when we create the Button object, so we can just remember its ID like so:
button = Button(style=ButtonStyle.blue, label="Next", emoji="➡")
button_id = button.custom_id
await ctx.send(link, components=[button])
Second, we'll need to pass a function to wait_for as the check keyword argument that only returns True if the button clicked was our button. Something like so:
def check_interaction(interaction):
return interaction.custom_id == button_id
await bot.wait_for("button_click", check=check_interaction)
Now your command should only respond to its own button presses :D
I've been trying to get this working for a long time now and i always get stuck at detecting button presses. I made a toast notification that looks like this:
Here's my code :
import winrt.windows.ui.notifications as notifications
import winrt.windows.data.xml.dom as dom
app = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe'
#create notifier
nManager = notifications.ToastNotificationManager
notifier = nManager.create_toast_notifier(app)
#define your notification as string
tString = """
<toast>
<visual>
<binding template='ToastGeneric'>
<text>New notifications</text>
<text>Text</text>
<text>Second text</text>
</binding>
</visual>
<actions>
<action
content="test1"
arguments="test1"
activationType="backround"/>
<action
content="test2"
arguments="test2"
activationType="backround"/>
</actions>
</toast>
"""
print(type(notifier.update))
#convert notification to an XmlDocument
xDoc = dom.XmlDocument()
xDoc.load_xml(tString)
#display notification
notifier.show(notifications.ToastNotification(xDoc))
I don't know how to detect button presses
the only thing i figured out is that if i change the argument of the buttons to a link like this:
arguments="https://google.com"
then it will open it
Is there any way i could implement this? or is there documentation for this XML format these toast notifications use. That explains how arguments work?
Alright so I know It's been a while, but I was trying to figure out the same thing and I couldn't find a good, conclusive answer anywhere. I've finally gotten something to work with WinRT in Python 3.9 so I wanted there to be an answer somewhere that people could find!
So to start, I'm not intimately familiar with how the 'arguments' attribute works, but it doesn't seem to be important for at least simple use cases. Most of what I know came from the Windows Toast docs. Here's some code that should produce a notification and open your Documents folder when you click the button. I got a headstart from an answer in this thread but it was missing some very important steps.
import os,sys,time
import subprocess
import threading
import winrt.windows.ui.notifications as notifications
import winrt.windows.data.xml.dom as dom
# this is not called on the main thread!
def handle_activated(sender, _):
path = os.path.expanduser("~\Documents")
subprocess.Popen('explorer "{}"'.format(path))
def test_notification():
#define your notification as
tString = """
<toast duration="short">
<visual>
<binding template='ToastGeneric'>
<text>New notifications</text>
<text>Text</text>
<text>Second text</text>
</binding>
</visual>
<actions>
<action
content="Test Button!"
arguments=""
activationType="foreground"/>
</actions>
</toast>
"""
#convert notification to an XmlDocument
xDoc = dom.XmlDocument()
xDoc.load_xml(tString)
notification = notifications.ToastNotification(xDoc)
# add the activation token.
notification.add_activated(handle_activated)
#create notifier
nManager = notifications.ToastNotificationManager
#link it to your Python executable (or whatever you want I guess?)
notifier = nManager.create_toast_notifier(sys.executable)
#display notification
notifier.show(notification)
duration = 7 # "short" duration for Toast notifications
# We have to wait for the results from the notification
# If we don't, the program will just continue and maybe even end before a button is clicked
thread = threading.Thread(target=lambda: time.sleep(duration))
thread.start()
print("We can still do things while the notification is displayed")
if __name__=="__main__":
test_notification()
The key thing to note here is that you need to find a way to wait for the response to the notification, since the notification is handled by a different thread than the program that produces it. This is why your "www.google.com" example worked while others didn't, because it didn't have anything to do with the Python program.
There's likely a more elegant solution, but a quick and easy way is to just create a Python thread and wait there for a duration. This way it doesn't interfere with the rest of your program in case you need to be doing something else. If you want your program to wait for a response, use time.sleep(duration) without all the threading code to pause the whole program.
I'm not sure how it works exactly, but it seems like the add_activated function just assigns a callback handler to the next available block in the XML. So if you wanted to add another button, my guess is that you can just do add_activated with another callback handler in the same order as you've listed your buttons.
Edit: I played around with it some and it turns out this lets you click anywhere, not just on the button. Not sure where to go from there but it's worth a heads up.
New Python(Windows) user here...
I am working with the wx.wizard components and have ran into a bit of a roadblock.
Typically the RunWizard property gets ran and everything is controlled by the event handler.
Rather than having the user manually click the next button, I had a scenario where I wanted to click it for them.
I've spend hours trying to figure this out and here is my best attempt at it so far.
Essentially what I need to do is invoke the wx.EVT_BUTTON event on this button... but I haven't been able to make it work.
Any help would be greatly appreciated.
def on_page_changed(self, evt):
'''Executed after the page has changed.'''
if evt.GetDirection(): dir = "forward"
else: dir = "backward"
page = evt.GetPage()
print "page_changed: %s, %s\n" % (dir, page.__class__)
nextbutton = mywiz.FindWindowById(wx.ID_FORWARD)
button_event = wx.PyCommandEvent(wx.EVT_BUTTON.typeId, self.Id)
button_event.EventObject = self
wx.PostEvent(self, button_event)
if page is self.pages[0]:
nextbutton = mywiz.FindWindowById(wx.ID_FORWARD)
button_event = wx.PyCommandEvent(wx.EVT_BUTTON.typeId, self.Id)
button_event.EventObject = self
wx.PostEvent(self, button_event)
Sounds like under some condition you want to skip a page. Look at the wxPython demo 'Run Dynamic Wizard' how this can be done.
In SublimeText 2, which uses Python plugins. I am trying to enhance an existing plugin that I found.
Basically it is a timer plugin that has start, stop, pause functionality and will print the Times to the Status bar using this Sublimetext API call...
sublime.status_message(TIMER)
What I would like to do is show something in the Status Bar to show that a Timer is in fact started and running. Something like this...
sublime.status_message('Timer: on')
The problem is this just briefly shows my Status bar message for a few seconds before being dismissed.
So I am looking for information on how to print to the status bar and keep it there long term?
You can use view.set_status(key, value) to place a persisting message in the status bar. However, this is bound to a view, not the application. If you need the message independent of the view, you will have to do some work using activated and deactivated listeners. Alternatively, you can set the status on all views in the window by using window.views() to get an array of all the views in the window, and place the status message on all of the views. Then, when you are done, remove all of the status messages.
please reference below code.
This is a class that clear the status after specified time period expired, and to use this class, just call set_status() function.
import time, threading
class StatusDisplay:
def __init__(self):
self.timeout = 3
self.timer = None
def set_status(self, view, msg, overwrite):
self.cancel_timer()
self.view = view
if overwrite:
self.view.set_status("mytag", msg+'\n')
else:
self.view.set_status("mytag", msg)
self.start_timer()
def cancel_timer(self):
if self.timer != None:
self.timer.cancel()
def start_timer(self):
self.timer = threading.Timer(self.timeout, self.clear)
self.timer.start()
def clear(self):
print('clear message')
self.view.erase_status("mytag")
I haven't really run across any plugins that can keep a message in the status bar for very long. This is probably an intentional design decision, so users can access the status bar menus, and plugins don't hijack other messages that need to be shown, like the cursor position.
Check out ScopeHunter for options on printing messages to different places. In particular, via user setting it can print to the status bar, clipboard, a panel, and/or the console log.
I've made a runnable example that demonstrates the buggy behavior: http://pastebin.com/8KpzD4pw
This issue is EXTREMELY aggravating. I have a wx.ProgressDialog up while I'm saving a file, and upon IOError, I want to close the progress dialog and display an error message. Unfortunately, this seems to be impossible. The progress dialog blatantly refuses to close before the message box closes:
As you can see, the message box appears below the progress dialog, so the user has to manually switch focus to the message box to see it's contents. When the message box is closed, the progress dialog disappears as well. Here is the code for the save function:
def save(self, path=None):
# Show a loading dialog while the document is staving.
progress = shared.show_loading(self, 'Saving document')
try:
raise IOError('Error message')
if not path:
self.document.save()
else:
self.document.save_to_file(path)
except IOError as e:
progress.done()
message = 'Failed to save file:\n\n{}'.format(e.message)
wx.MessageBox(message, 'Error', wx.OK | wx.ICON_ERROR)
progress.done()
The show_loading and progress.done functions are just shortcuts for using the wx.ProgressDialog (source).
Why does the progress dialog not disappear before the message box is opened? How can I fix it?
I have also tried using wx.CallAfter to open the message box, to no avail:
# ...
except IOError as e:
message = 'Failed to save file:\n\n{}'.format(e.message)
def show_error():
wx.MessageBox(message, 'Error', wx.OK | wx.ICON_ERROR)
progress.done()
wx.CallAfter(show_error)
# ...
I have also tried to sleep for 100ms between closing the progress dialog and opening the message box using wx.MicroSleep without success.
I have also tried calling wx.Yield() and wx.WakeUpIdle() right after destroying the progress dialog, neither having any effect.
Just out of curiosity... Have you tried using wx.SafeYield() or wx.Yield() or wx.YieldIfNeeded() right after the call to progressdialog.Destroy()?
Your sample is not runnable as it stands so I am just shooting in the dark.
I think Infinity77 has the right answer here. People forget that GUI calls are not synchronous -- they aren't finished by the time they return. That "done" call sends a message to the window, and in response to that, the window probably queues up several more messages to clean itself up. When you fire up a model message box, that creates its OWN message loop, while leaving the original message loop in suspended animation. Thus, the cleanup messages cannot be processed until the message box returns and your main message loop runs again. A Yield call will allow those queued up messages to drain.
I had a similar case, which I finally resolved by calling:
dlg.Update( dlg.GetRange( ) )
It seems that, at least when you put the progress dialog into "pulse" mode, it won't immediately respond to Destroy calls. No amount of sleeping or yielding before or after destroying it would convince my progress dialog to stop displaying. However, by instead simply updating the value to the max, it seems to automatically destroy (or at least hide) itself immediately.
The wxPython demo shows how to interrupt a ProgressDialog. It also shows that you need to Destroy() it instead of Close() it, which is the normal way of getting rid of dialogs. In your exception handler, you will want to stop whatever the ProgressDialog is keeping track of and Destroy() it. Then show your MessageBox.
I have figured out a workaround. It turns out that I can't remove the native windows progress dialog right after I create it. I have to wait a while, probably for the dialog to be completely initialized, before I'm allowed to destroy it. I added this code:
wx.MilliSleep(50)
Into my progress dialog shortcut function, which introduces an unnoticeable delay after opening the progress dialog and allows me to destroy the progress dialog when ever I want.
Complete shortcut function:
def show_loading(parent, title, message=None, maximum=100):
if not message:
message = title
# A class for the return value.
class LoadingObject(object):
def __init__(self, dialog):
self.dialog = dialog
self.is_done = False
def done(self):
if not self.is_done:
self.dialog.Destroy()
self.is_done = True
def pulse(self, message):
self.dialog.Pulse(message)
def progress(self, current, message=None):
# Don't allow the progress to reach 100%, since it will freeze the
# dialog.
if current >= maximum:
current = current - 1
if message is not None:
self.dialog.Update(current, message)
else:
self.dialog.Update(current)
# Create the progress dialog.
dlg_style = wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME
dlg = wx.ProgressDialog(
title, message, parent=parent, style=dlg_style, maximum=maximum
)
dlg.Pulse()
# Wait just a little bit to allow the progress dialog to initialize.
wx.MilliSleep(50)
# Return an instance of the LoadingDialog with the progress dialog attached.
return LoadingObject(dlg)
Final save function:
def save(self, path=None):
# Show a loading dialog while the document is staving.
progress = shared.show_loading(self, 'Saving document')
try:
if not path:
self.document.save()
else:
self.document.save_to_file(path)
except IOError as e:
message = 'Failed to save file:\n\n{}'.format(e.message)
wx.MessageBox(message, 'Error', wx.OK | wx.ICON_ERROR)
finally:
progress.done()
I took a slightly different approach to this problem. Had multiple function calls in a linear order, and wanted to show the overall progress as function calls were done. I simply wrapped the progress bar in a function, taking the function to call and its arguments. On exception I destroy the progress bar, and raise the exception. Example below:
def _progress_wrap(self, func, *args, **kwargs):
self.count += 1
self.progress.Update(self.count)
res = None
try:
res = func(*args, **kwargs)
except Exception:
self.progress.Destroy()
raise
return(res)