##// END OF EJS Templates
add gtk3 support to ipython kernel
John Stowers -
Show More
@@ -0,0 +1,85 b''
1 """GUI support for the IPython ZeroMQ kernel - GTK toolkit support.
2 """
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2010-2011 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING.txt, distributed as part of this software.
8 #-----------------------------------------------------------------------------
9
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
13 # stdlib
14 import sys
15
16 # Third-party
17 from gi.repository import GObject, Gtk
18
19 #-----------------------------------------------------------------------------
20 # Classes and functions
21 #-----------------------------------------------------------------------------
22
23 class GTKEmbed(object):
24 """A class to embed a kernel into the GTK main event loop.
25 """
26 def __init__(self, kernel):
27 self.kernel = kernel
28 # These two will later store the real gtk functions when we hijack them
29 self.gtk_main = None
30 self.gtk_main_quit = None
31
32 def start(self):
33 """Starts the GTK main event loop and sets our kernel startup routine.
34 """
35 # Register our function to initiate the kernel and start gtk
36 GObject.idle_add(self._wire_kernel)
37 Gtk.main()
38
39 def _wire_kernel(self):
40 """Initializes the kernel inside GTK.
41
42 This is meant to run only once at startup, so it does its job and
43 returns False to ensure it doesn't get run again by GTK.
44 """
45 self.gtk_main, self.gtk_main_quit = self._hijack_gtk()
46 GObject.timeout_add(int(1000*self.kernel._poll_interval),
47 self.iterate_kernel)
48 return False
49
50 def iterate_kernel(self):
51 """Run one iteration of the kernel and return True.
52
53 GTK timer functions must return True to be called again, so we make the
54 call to :meth:`do_one_iteration` and then return True for GTK.
55 """
56 self.kernel.do_one_iteration()
57 return True
58
59 def stop(self):
60 # FIXME: this one isn't getting called because we have no reliable
61 # kernel shutdown. We need to fix that: once the kernel has a
62 # shutdown mechanism, it can call this.
63 self.gtk_main_quit()
64 sys.exit()
65
66 def _hijack_gtk(self):
67 """Hijack a few key functions in GTK for IPython integration.
68
69 Modifies pyGTK's main and main_quit with a dummy so user code does not
70 block IPython. This allows us to use %run to run arbitrary pygtk
71 scripts from a long-lived IPython session, and when they attempt to
72 start or stop
73
74 Returns
75 -------
76 The original functions that have been hijacked:
77 - Gtk.main
78 - Gtk.main_quit
79 """
80 def dummy(*args, **kw):
81 pass
82 # save and trap main and main_quit from gtk
83 orig_main, Gtk.main = Gtk.main, dummy
84 orig_main_quit, Gtk.main_quit = Gtk.main_quit, dummy
85 return orig_main, orig_main_quit
@@ -1,255 +1,264 b''
1 1 # encoding: utf-8
2 2 """Event loop integration for the ZeroMQ-based kernels.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (C) 2011 The IPython Development Team
7 7
8 8 # Distributed under the terms of the BSD License. The full license is in
9 9 # the file COPYING, distributed as part of this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import sys
18 18
19 19 # System library imports
20 20 import zmq
21 21
22 22 # Local imports
23 23 from IPython.config.application import Application
24 24 from IPython.utils import io
25 25
26 26
27 27 #------------------------------------------------------------------------------
28 28 # Eventloops for integrating the Kernel into different GUIs
29 29 #------------------------------------------------------------------------------
30 30
31 31 def _on_os_x_10_9():
32 32 import platform
33 33 from distutils.version import LooseVersion as V
34 34 return sys.platform == 'darwin' and V(platform.mac_ver()[0]) >= V('10.9')
35 35
36 36 def _notify_stream_qt(kernel, stream):
37 37
38 38 from IPython.external.qt_for_kernel import QtCore
39 39
40 40 if _on_os_x_10_9() and kernel._darwin_app_nap:
41 41 from IPython.external.appnope import nope_scope as context
42 42 else:
43 43 from IPython.core.interactiveshell import NoOpContext as context
44 44
45 45 def process_stream_events():
46 46 while stream.getsockopt(zmq.EVENTS) & zmq.POLLIN:
47 47 with context():
48 48 kernel.do_one_iteration()
49 49
50 50 fd = stream.getsockopt(zmq.FD)
51 51 notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, kernel.app)
52 52 notifier.activated.connect(process_stream_events)
53 53
54 54 def loop_qt4(kernel):
55 55 """Start a kernel with PyQt4 event loop integration."""
56 56
57 57 from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
58 58
59 59 kernel.app = get_app_qt4([" "])
60 60 kernel.app.setQuitOnLastWindowClosed(False)
61 61
62 62 for s in kernel.shell_streams:
63 63 _notify_stream_qt(kernel, s)
64 64
65 65 start_event_loop_qt4(kernel.app)
66 66
67 67
68 68 def loop_wx(kernel):
69 69 """Start a kernel with wx event loop support."""
70 70
71 71 import wx
72 72 from IPython.lib.guisupport import start_event_loop_wx
73 73
74 74 if _on_os_x_10_9() and kernel._darwin_app_nap:
75 75 # we don't hook up App Nap contexts for Wx,
76 76 # just disable it outright.
77 77 from IPython.external.appnope import nope
78 78 nope()
79 79
80 80 doi = kernel.do_one_iteration
81 81 # Wx uses milliseconds
82 82 poll_interval = int(1000*kernel._poll_interval)
83 83
84 84 # We have to put the wx.Timer in a wx.Frame for it to fire properly.
85 85 # We make the Frame hidden when we create it in the main app below.
86 86 class TimerFrame(wx.Frame):
87 87 def __init__(self, func):
88 88 wx.Frame.__init__(self, None, -1)
89 89 self.timer = wx.Timer(self)
90 90 # Units for the timer are in milliseconds
91 91 self.timer.Start(poll_interval)
92 92 self.Bind(wx.EVT_TIMER, self.on_timer)
93 93 self.func = func
94 94
95 95 def on_timer(self, event):
96 96 self.func()
97 97
98 98 # We need a custom wx.App to create our Frame subclass that has the
99 99 # wx.Timer to drive the ZMQ event loop.
100 100 class IPWxApp(wx.App):
101 101 def OnInit(self):
102 102 self.frame = TimerFrame(doi)
103 103 self.frame.Show(False)
104 104 return True
105 105
106 106 # The redirect=False here makes sure that wx doesn't replace
107 107 # sys.stdout/stderr with its own classes.
108 108 kernel.app = IPWxApp(redirect=False)
109 109
110 110 # The import of wx on Linux sets the handler for signal.SIGINT
111 111 # to 0. This is a bug in wx or gtk. We fix by just setting it
112 112 # back to the Python default.
113 113 import signal
114 114 if not callable(signal.getsignal(signal.SIGINT)):
115 115 signal.signal(signal.SIGINT, signal.default_int_handler)
116 116
117 117 start_event_loop_wx(kernel.app)
118 118
119 119
120 120 def loop_tk(kernel):
121 121 """Start a kernel with the Tk event loop."""
122 122
123 123 try:
124 124 from tkinter import Tk # Py 3
125 125 except ImportError:
126 126 from Tkinter import Tk # Py 2
127 127 doi = kernel.do_one_iteration
128 128 # Tk uses milliseconds
129 129 poll_interval = int(1000*kernel._poll_interval)
130 130 # For Tkinter, we create a Tk object and call its withdraw method.
131 131 class Timer(object):
132 132 def __init__(self, func):
133 133 self.app = Tk()
134 134 self.app.withdraw()
135 135 self.func = func
136 136
137 137 def on_timer(self):
138 138 self.func()
139 139 self.app.after(poll_interval, self.on_timer)
140 140
141 141 def start(self):
142 142 self.on_timer() # Call it once to get things going.
143 143 self.app.mainloop()
144 144
145 145 kernel.timer = Timer(doi)
146 146 kernel.timer.start()
147 147
148 148
149 149 def loop_gtk(kernel):
150 150 """Start the kernel, coordinating with the GTK event loop"""
151 151 from .gui.gtkembed import GTKEmbed
152 152
153 153 gtk_kernel = GTKEmbed(kernel)
154 154 gtk_kernel.start()
155 155
156 156
157 def loop_gtk3(kernel):
158 """Start the kernel, coordinating with the GTK event loop"""
159 from .gui.gtk3embed import GTKEmbed
160
161 gtk_kernel = GTKEmbed(kernel)
162 gtk_kernel.start()
163
164
157 165 def loop_cocoa(kernel):
158 166 """Start the kernel, coordinating with the Cocoa CFRunLoop event loop
159 167 via the matplotlib MacOSX backend.
160 168 """
161 169 import matplotlib
162 170 if matplotlib.__version__ < '1.1.0':
163 171 kernel.log.warn(
164 172 "MacOSX backend in matplotlib %s doesn't have a Timer, "
165 173 "falling back on Tk for CFRunLoop integration. Note that "
166 174 "even this won't work if Tk is linked against X11 instead of "
167 175 "Cocoa (e.g. EPD). To use the MacOSX backend in the kernel, "
168 176 "you must use matplotlib >= 1.1.0, or a native libtk."
169 177 )
170 178 return loop_tk(kernel)
171 179
172 180 from matplotlib.backends.backend_macosx import TimerMac, show
173 181
174 182 # scale interval for sec->ms
175 183 poll_interval = int(1000*kernel._poll_interval)
176 184
177 185 real_excepthook = sys.excepthook
178 186 def handle_int(etype, value, tb):
179 187 """don't let KeyboardInterrupts look like crashes"""
180 188 if etype is KeyboardInterrupt:
181 189 io.raw_print("KeyboardInterrupt caught in CFRunLoop")
182 190 else:
183 191 real_excepthook(etype, value, tb)
184 192
185 193 # add doi() as a Timer to the CFRunLoop
186 194 def doi():
187 195 # restore excepthook during IPython code
188 196 sys.excepthook = real_excepthook
189 197 kernel.do_one_iteration()
190 198 # and back:
191 199 sys.excepthook = handle_int
192 200
193 201 t = TimerMac(poll_interval)
194 202 t.add_callback(doi)
195 203 t.start()
196 204
197 205 # but still need a Poller for when there are no active windows,
198 206 # during which time mainloop() returns immediately
199 207 poller = zmq.Poller()
200 208 if kernel.control_stream:
201 209 poller.register(kernel.control_stream.socket, zmq.POLLIN)
202 210 for stream in kernel.shell_streams:
203 211 poller.register(stream.socket, zmq.POLLIN)
204 212
205 213 while True:
206 214 try:
207 215 # double nested try/except, to properly catch KeyboardInterrupt
208 216 # due to pyzmq Issue #130
209 217 try:
210 218 # don't let interrupts during mainloop invoke crash_handler:
211 219 sys.excepthook = handle_int
212 220 show.mainloop()
213 221 sys.excepthook = real_excepthook
214 222 # use poller if mainloop returned (no windows)
215 223 # scale by extra factor of 10, since it's a real poll
216 224 poller.poll(10*poll_interval)
217 225 kernel.do_one_iteration()
218 226 except:
219 227 raise
220 228 except KeyboardInterrupt:
221 229 # Ctrl-C shouldn't crash the kernel
222 230 io.raw_print("KeyboardInterrupt caught in kernel")
223 231 finally:
224 232 # ensure excepthook is restored
225 233 sys.excepthook = real_excepthook
226 234
227 235 # mapping of keys to loop functions
228 236 loop_map = {
229 237 'qt' : loop_qt4,
230 238 'qt4': loop_qt4,
231 239 'inline': None,
232 240 'osx': loop_cocoa,
233 241 'wx' : loop_wx,
234 242 'tk' : loop_tk,
235 243 'gtk': loop_gtk,
244 'gtk3': loop_gtk3,
236 245 None : None,
237 246 }
238 247
239 248
240 249 def enable_gui(gui, kernel=None):
241 250 """Enable integration with a given GUI"""
242 251 if gui not in loop_map:
243 252 e = "Invalid GUI request %r, valid ones are:%s" % (gui, loop_map.keys())
244 253 raise ValueError(e)
245 254 if kernel is None:
246 255 if Application.initialized():
247 256 kernel = getattr(Application.instance(), 'kernel', None)
248 257 if kernel is None:
249 258 raise RuntimeError("You didn't specify a kernel,"
250 259 " and no IPython Application with a kernel appears to be running."
251 260 )
252 261 loop = loop_map[gui]
253 262 if loop and kernel.eventloop is not None and kernel.eventloop is not loop:
254 263 raise RuntimeError("Cannot activate multiple GUI eventloops")
255 264 kernel.eventloop = loop
General Comments 0
You need to be logged in to leave comments. Login now