Permalink
Browse files

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.
  • Loading branch information...
1 parent f0e4ac0 commit 13751b1c86126f974f13041a10f8da3920e3335c @fperez fperez committed Sep 4, 2010
Showing with 130 additions and 10 deletions.
  1. +15 −0 IPython/zmq/gui/__init__.py
  2. +86 −0 IPython/zmq/gui/gtkembed.py
  3. +29 −10 IPython/zmq/ipkernel.py
View
15 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.
+#-----------------------------------------------------------------------------
View
86 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
View
39 IPython/zmq/ipkernel.py
@@ -88,6 +88,8 @@ def __init__(self, **kwargs):
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 @@ def start(self):
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 @@ def __init__(self, func):
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,31 +414,45 @@ def start(self):
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()
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 @@ def main():
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)

0 comments on commit 13751b1

Please sign in to comment.