From 13751b1c86126f974f13041a10f8da3920e3335c 2010-09-04 23:14:46 From: Fernando Perez Date: 2010-09-04 23:14:46 Subject: [PATCH] Added GTK support to ZeroMQ kernel. We use an approach which is a combination of an gtk timer callback into our execution loop, like we do for Qt and Wx, I've run as tests several GTK examples found on the net, as well as multiple matplotlib scripts, and so far everything works as expected. The only catch is that we silently trap gtk.main_quit(), so examples that call it with a 'close' button or similar seem to not do anything. But their windows close normally and no other problems have been found. This solution uses code taken from an old bug report of ours: https://bugs.launchpad.net/ipython/+bug/270856 specifically the attachment in this comment: https://bugs.launchpad.net/ipython/+bug/270856/comments/6 along with the changes suggested by Michiel de Hoon there. Thanks to Ville and Michiel for that old discussion, which put me on the right track to figure out the details of the logic needed for GTK. --- diff --git a/IPython/zmq/gui/__init__.py b/IPython/zmq/gui/__init__.py new file mode 100644 index 0000000..91037ba --- /dev/null +++ b/IPython/zmq/gui/__init__.py @@ -0,0 +1,15 @@ +"""GUI support for the IPython ZeroMQ kernel. + +This package contains the various toolkit-dependent utilities we use to enable +coordination between the IPython kernel and the event loops of the various GUI +toolkits. +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2010 The IPython Development Team. +# +# Distributed under the terms of the BSD License. +# +# The full license is in the file COPYING.txt, distributed as part of this +# software. +#----------------------------------------------------------------------------- diff --git a/IPython/zmq/gui/gtkembed.py b/IPython/zmq/gui/gtkembed.py new file mode 100644 index 0000000..4d33663 --- /dev/null +++ b/IPython/zmq/gui/gtkembed.py @@ -0,0 +1,86 @@ +"""GUI support for the IPython ZeroMQ kernel - GTK toolkit support. +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2010 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING.txt, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- +# stdlib +import sys + +# Third-party +import gobject +import gtk + +#----------------------------------------------------------------------------- +# Classes and functions +#----------------------------------------------------------------------------- + +class GTKEmbed(object): + """A class to embed a kernel into the GTK main event loop. + """ + def __init__(self, kernel): + self.kernel = kernel + # These two will later store the real gtk functions when we hijack them + self.gtk_main = None + self.gtk_main_quit = None + + def start(self): + """Starts the GTK main event loop and sets our kernel startup routine. + """ + # Register our function to initiate the kernel and start gtk + gobject.idle_add(self._wire_kernel) + gtk.main() + + def _wire_kernel(self): + """Initializes the kernel inside GTK. + + This is meant to run only once at startup, so it does its job and + returns False to ensure it doesn't get run again by GTK. + """ + self.gtk_main, self.gtk_main_quit = self._hijack_gtk() + gobject.timeout_add(int(1000*self.kernel._poll_interval), + self.iterate_kernel) + return False + + def iterate_kernel(self): + """Run one iteration of the kernel and return True. + + GTK timer functions must return True to be called again, so we make the + call to :meth:`do_one_iteration` and then return True for GTK. + """ + self.kernel.do_one_iteration() + return True + + def stop(self): + # FIXME: this one isn't getting called because we have no reliable + # kernel shutdown. We need to fix that: once the kernel has a + # shutdown mechanism, it can call this. + self.gtk_main_quit() + sys.exit() + + def _hijack_gtk(self): + """Hijack a few key functions in GTK for IPython integration. + + Modifies pyGTK's main and main_quit with a dummy so user code does not + block IPython. This allows us to use %run to run arbitrary pygtk + scripts from a long-lived IPython session, and when they attempt to + start or stop + + Returns + ------- + The original functions that have been hijacked: + - gtk.main + - gtk.main_quit + """ + def dummy(*args, **kw): + pass + # save and trap main and main_quit from gtk + orig_main, gtk.main = gtk.main, dummy + orig_main_quit, gtk.main_quit = gtk.main_quit, dummy + return orig_main, orig_main_quit diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 5f51f78..5105f95 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -88,6 +88,8 @@ class Kernel(Configurable): self.handlers[msg_type] = getattr(self, msg_type) def do_one_iteration(self): + """Do one iteration of the kernel's evaluation loop. + """ try: ident = self.reply_socket.recv(zmq.NOBLOCK) except zmq.ZMQError, e: @@ -373,7 +375,8 @@ class WxKernel(Kernel): import wx from IPython.lib.guisupport import start_event_loop_wx doi = self.do_one_iteration - _poll_interval = self._poll_interval + # Wx uses milliseconds + poll_interval = int(1000*self._poll_interval) # We have to put the wx.Timer in a wx.Frame for it to fire properly. # We make the Frame hidden when we create it in the main app below. @@ -382,9 +385,10 @@ class WxKernel(Kernel): wx.Frame.__init__(self, None, -1) self.timer = wx.Timer(self) # Units for the timer are in milliseconds - self.timer.Start(1000*_poll_interval) + self.timer.Start(poll_interval) self.Bind(wx.EVT_TIMER, self.on_timer) self.func = func + def on_timer(self, event): self.func() @@ -410,17 +414,19 @@ class TkKernel(Kernel): import Tkinter doi = self.do_one_iteration - + # Tk uses milliseconds + poll_interval = int(1000*self._poll_interval) # For Tkinter, we create a Tk object and call its withdraw method. class Timer(object): def __init__(self, func): self.app = Tkinter.Tk() self.app.withdraw() self.func = func + def on_timer(self): self.func() - # Units for the timer are in milliseconds - self.app.after(1000*self._poll_interval, self.on_timer) + self.app.after(poll_interval, self.on_timer) + def start(self): self.on_timer() # Call it once to get things going. self.app.mainloop() @@ -428,13 +434,25 @@ class TkKernel(Kernel): self.timer = Timer(doi) self.timer.start() + +class GTKKernel(Kernel): + """A Kernel subclass with GTK support.""" + + def start(self): + """Start the kernel, coordinating with the GTK event loop""" + from .gui.gtkembed import GTKEmbed + + gtk_kernel = GTKEmbed(self) + gtk_kernel.start() + + #----------------------------------------------------------------------------- # Kernel main and launch functions #----------------------------------------------------------------------------- def launch_kernel(xrep_port=0, pub_port=0, req_port=0, hb_port=0, independent=False, pylab=False): - """ Launches a localhost kernel, binding to the specified ports. + """Launches a localhost kernel, binding to the specified ports. Parameters ---------- @@ -490,19 +508,20 @@ given, the GUI backend is matplotlib's, otherwise use one of: \ kernel_class = Kernel - _kernel_classes = { + kernel_classes = { 'qt' : QtKernel, - 'qt4' : QtKernel, + 'qt4': QtKernel, 'payload-svg': Kernel, 'wx' : WxKernel, - 'tk' : TkKernel + 'tk' : TkKernel, + 'gtk': GTKKernel, } if namespace.pylab: if namespace.pylab == 'auto': gui, backend = pylabtools.find_gui_and_backend() else: gui, backend = pylabtools.find_gui_and_backend(namespace.pylab) - kernel_class = _kernel_classes.get(gui) + kernel_class = kernel_classes.get(gui) if kernel_class is None: raise ValueError('GUI is not supported: %r' % gui) pylabtools.activate_matplotlib(backend)