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)