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.
Related
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.
I want to create a box were the user is informed of what the application is actually doing.
I created a Text Widget were to show the print statements that I wrote in key points of the applications, so that it could serve as a log box.
To do this, I redirected the stdout to a subclass of the widget itself "upgraded" with a write method as I saw here in another post.
This does indeed work, but I noticed a problem that makes the box almost useless.
If you run the code, you can see that the sentences appear all at once. More puzzling for me is that not only the sentences of the "wait2" functions
appear togheter, but even the print statement of the calling function, "wait1", is shown at the end of the process.
Why this behaviour? what can I do to see the statement shown in the box as they are executed?
from Tkinter import *
import sys
import time
root = Tk()
class addwritemethod(object):
def __init__(self, widget):
self.widget = widget
def write(self, string):
self.widget.configure(state="normal")
self.widget.insert("end", string)
self.widget.see("end")
self.widget.configure(state="disabled")
def wait1():
print "Can you see me?"
wait2()
def wait2():
for i in range(10):
time.sleep(5)
print "Long time no see!"
t_go = Button(root, text= "Start!", width =12, command = wait1)
t_go.pack(side = LEFT, padx = 20, pady = 20)
tlog = Text(root,wrap = "word")
tlog.pack(side="top", fill="both", expand=True)
tlog.configure(state="disabled")
sys.stdout = addwritemethod(tlog)
mainloop()
EDIT: I want to say thanks to the people who answered me and an apology: I did not give all the required information.
I put time.sleep() in the test code only to show you the behaviour. In the real application, I trasfer a file via ssh with Paramiko and I don't use sleep().
Maybe I choose the wrong example, but the result is the same, all the print stament are shown at the same moment.
You could also use the built-in logging module to achieve your goal of
creating a box where the user is informed of what the application is actually doing ... a log box.
I had this same need and converged on the recommendations provided here and here.
I have an example below that I created to illustrate the concept of logging to a GUI control using Tkinter. The example below logs to a text control as you ask, but you can send log messages to other GUI components by replacing/copying the class MyHandlerText with other handler classes like MyHandlerLabel, MyHandlerListbox, etc. (choose your own names for the handler classes). Then you'd have a handler for a variety of GUI controls of interest. The big "a-ha" moment for me was the module-level getLogger concept encouraged by python.org.
import Tkinter
import logging
import datetime
# this item "module_logger" is visible only in this module,
# (but you can create references to the same logger object from other modules
# by calling getLogger with an argument equal to the name of this module)
# this way, you can share or isolate loggers as desired across modules and across threads
# ...so it is module-level logging and it takes the name of this module (by using __name__)
# recommended per https://docs.python.org/2/library/logging.html
module_logger = logging.getLogger(__name__)
class simpleapp_tk(Tkinter.Tk):
def __init__(self,parent):
Tkinter.Tk.__init__(self,parent)
self.parent = parent
self.grid()
self.mybutton = Tkinter.Button(self, text="ClickMe")
self.mybutton.grid(column=0,row=0,sticky='EW')
self.mybutton.bind("<ButtonRelease-1>", self.button_callback)
self.mytext = Tkinter.Text(self, state="disabled")
self.mytext.grid(column=0, row=1)
def button_callback(self, event):
now = datetime.datetime.now()
module_logger.info(now)
class MyHandlerText(logging.StreamHandler):
def __init__(self, textctrl):
logging.StreamHandler.__init__(self) # initialize parent
self.textctrl = textctrl
def emit(self, record):
msg = self.format(record)
self.textctrl.config(state="normal")
self.textctrl.insert("end", msg + "\n")
self.flush()
self.textctrl.config(state="disabled")
if __name__ == "__main__":
# create Tk object instance
app = simpleapp_tk(None)
app.title('my application')
# setup logging handlers using the Tk instance created above
# the pattern below can be used in other threads...
# ...to allow other thread to send msgs to the gui
# in this example, we set up two handlers just for demonstration (you could add a fileHandler, etc)
stderrHandler = logging.StreamHandler() # no arguments => stderr
module_logger.addHandler(stderrHandler)
guiHandler = MyHandlerText(app.mytext)
module_logger.addHandler(guiHandler)
module_logger.setLevel(logging.INFO)
module_logger.info("from main")
# start Tk
app.mainloop()
When you call sleep, the application does exactly that: it sleeps. When it's sleeping it can't update the display. As a general rule you should never call sleep in a GUI.
That being said, a quick fix is to make sure you call update after printing something to the log, so that Tkinter has a chance to update the screen. Add self.widget.update_idletasks() at the end of write (redrawing the screen is considered an "idle task").
This isn't a proper fix but it's good enough to illustrate why the data isn't appearing. A proper fix involves not calling sleep. There are many examples on stackoverflow related to this, and almost all of them involve using the after method.
I am trying to code an application that consists of various windows (e.g., generic message dialog, login dialog, main interface, etc.) and am having trouble getting the gtk.main_quit function to be called: either I get a complaint about the call being outside the main loop, or the function doesn't get called at all.
I am a newbie to both Python and GTK+, but my best guess as to how to get this to work is to have a "root" window, which is just a placeholder that is never seen, but controls the application's GTK+ loop. My code, so far, is as follows:
import pygtk
pygtk.require("2.0")
import gtk
class App(gtk.Window):
_exitStatus = 0
# Generic message box
def msg(self, title, text, type = gtk.MESSAGE_INFO, buttons = gtk.BUTTONS_OK):
# Must always have a button
if buttons == gtk.BUTTONS_NONE:
buttons = gtk.BUTTONS_OK
dialog = gtk.MessageDialog(None, 0, type, buttons, title)
dialog.set_title(title)
dialog.set_geometry_hints(min_width = 300)
dialog.set_resizable(False)
dialog.set_deletable(False)
dialog.set_position(gtk.WIN_POS_CENTER)
dialog.set_modal(True)
dialog.format_secondary_text(text)
response = dialog.run()
dialog.destroy()
return response
def nuke(self, widget, data):
gtk.main_quit()
exit(self._exitStatus)
def __init__(self):
super(App, self).__init__()
self.connect('destroy', self.nuke)
try:
raise Exception()
except:
self.msg('OMFG!', 'WTF just happened!?', gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE)
self._exitStatus = 1
self.destroy()
if self.msg('OK', 'Everything worked fine') == gtk.RESPONSE_OK:
self.destroy()
# Let's go!
App()
gtk.main()
The nuke function never gets called, despite the explicit calls to destroy.
DIFF On #DonQuestion's advice:
- self.destroy()
+ self.emit('destroy')
- App()
+ app = App()
This didn't solve the problem...
UPDATE Accepted #jku's answer, but also see my own answer for extra information...
First, there is a bit of a test problem with the code: You call Gtk.main_quit() from the App initialization: this happens before main loop is even running so signals probably won't work.
Second, you'll probably get a warning on destroy(): 'destroy' handler only takes two arguments (self plus one) but yours has three...
Also with regards to your comment about control flow: You don't need a Window to get signals as they're a GObject feature. And for your testing needs you could write a App.test_except() function and use glib.idle_add (self.test_except) in the object initialization -- this way test_except() is called when main loop is running.
I think #jku's answer identifies my key error, so I have marked it accepted, but while playing around, I found that the MessageDialog does not need to run within the GTK+ loop. I don't know if this is as designed, but it works! So, I broke my generic message dialog out into its own function and then kept the main app altogether in a class of its own, which respects the main loop as I was expecting:
import pygtk
pygtk.require("2.0")
import gtk
def msg(title, text, type = gtk.MESSAGE_INFO, buttons = gtk.BUTTONS_OK):
# Only allowed OK, Close, Cancel, Yes/No and OK/Cancel buttons
# Otherwise, default to just OK
if buttons not in [gtk.BUTTONS_OK, gtk.BUTTONS_CLOSE, gtk.BUTTONS_CANCEL, gtk.BUTTONS_YES_NO, gtk.BUTTONS_OK_CANCEL]:
buttons = gtk.BUTTONS_OK
dialog = gtk.MessageDialog(None, 0, type, buttons, title)
dialog.set_title(title)
dialog.set_geometry_hints(min_width = 300)
dialog.set_resizable(False)
dialog.set_deletable(False)
dialog.set_position(gtk.WIN_POS_CENTER)
dialog.set_modal(True)
dialog.format_secondary_text(text)
response = dialog.run()
dialog.destroy()
return response
class App:
def __init__(self):
# Build UI
# Connect signals
# Show whatever
def appQuit(self, widget):
gtk.main_quit()
def signalHandler(self, widget, data = None):
# Handle signal
# We can call msg here, when the main loop is running
# Load some resource
# We can call msg here, despite not having invoked the main loop
try:
# Load resource
except:
msg('OMFG!', 'WTF just happened!?', gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE)
exit(1)
# n.b., Calls to msg work even without the following code
App()
gtk.main()
exit(0)
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)
I'm trying to code something that downloads a file from a webserver and saves it, showing the download progress in a QProgressBar.
Now, there are ways to do this in regular Python and it's easy. Problem is that it locks the refresh of the progressBar. Solution is to use PyQT's QNetworkManager class. I can download stuff just fine with it, I just can't get the setup to show the progress on the progressBar. HereĀ“s an example:
class Form(QDialog):
def __init__(self,parent=None):
super(Form,self).__init__(parent)
self.progressBar = QProgressBar()
self.reply = None
layout = QHBoxLayout()
layout.addWidget(self.progressBar)
self.setLayout(layout)
self.manager = QNetworkAccessManager(self)
self.connect(self.manager,SIGNAL("finished(QNetworkReply*)"),self.replyFinished)
self.Down()
def Down(self):
address = QUrl("http://stackoverflow.com") #URL from the remote file.
self.manager.get(QNetworkRequest(address))
def replyFinished(self, reply):
self.connect(reply,SIGNAL("downloadProgress(int,int)"),self.progressBar, SLOT("setValue(int)"))
self.reply = reply
self.progressBar.setMaximum(reply.size())
alltext = self.reply.readAll()
#print alltext
#print alltext
def updateBar(self, read,total):
print "read", read
print "total",total
#self.progressBar.setMinimum(0)
#self.progressBar.setMask(total)
#self.progressBar.setValue(read)
In this case, my method "updateBar" is never called... any ideas?
Well you haven't connected any of the signals to your updateBar() method.
change
def replyFinished(self, reply):
self.connect(reply,SIGNAL("downloadProgress(int,int)"),self.progressBar, SLOT("setValue(int)"))
to
def replyFinished(self, reply):
self.connect(reply,SIGNAL("downloadProgress(int,int)"),self.updateBar)
Note that in Python you don't have to explicitly use the SLOT() syntax; you can just pass the reference to your method or function.
Update:
I just wanted to point out that if you want to use a Progress bar in any situation where your GUI locks up during processing, one solution is to run your processing code in another thread so your GUI receives repaint events. Consider reading about the QThread class, in case you come across another reason for a progress bar that does not have a pre-built solution for you.