##// END OF EJS Templates
inputhook: improve CTRL+C handling with qt4...
Christian Boos -
Show More
@@ -1,464 +1,483 b''
1 1 # coding: utf-8
2 2 """
3 3 Inputhook management for GUI event loop integration.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2009 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import ctypes
18 18 import os
19 19 import sys
20 20 import warnings
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Constants
24 24 #-----------------------------------------------------------------------------
25 25
26 26 # Constants for identifying the GUI toolkits.
27 27 GUI_WX = 'wx'
28 28 GUI_QT = 'qt'
29 29 GUI_QT4 = 'qt4'
30 30 GUI_GTK = 'gtk'
31 31 GUI_TK = 'tk'
32 32 GUI_OSX = 'osx'
33 33 GUI_GLUT = 'glut'
34 34 GUI_PYGLET = 'pyglet'
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Utilities
38 38 #-----------------------------------------------------------------------------
39 39
40 40 def stdin_ready():
41 41 if os.name == 'posix':
42 42 import select
43 43 infds, outfds, erfds = select.select([sys.stdin],[],[],0)
44 44 if infds:
45 45 return True
46 46 else:
47 47 return False
48 48 elif os.name == 'nt':
49 49 import msvcrt
50 50 return msvcrt.kbhit()
51 51 return True # assume there's something so that we won't wait forever
52 52
53 53
54 54 #-----------------------------------------------------------------------------
55 55 # Main InputHookManager class
56 56 #-----------------------------------------------------------------------------
57 57
58 58
59 59 class InputHookManager(object):
60 60 """Manage PyOS_InputHook for different GUI toolkits.
61 61
62 62 This class installs various hooks under ``PyOSInputHook`` to handle
63 63 GUI event loop integration.
64 64 """
65 65
66 66 def __init__(self):
67 67 self.PYFUNC = ctypes.PYFUNCTYPE(ctypes.c_int)
68 68 self._apps = {}
69 69 self._reset()
70 70
71 71 def _reset(self):
72 72 self._callback_pyfunctype = None
73 73 self._callback = None
74 74 self._installed = False
75 75 self._current_gui = None
76 76
77 77 def get_pyos_inputhook(self):
78 78 """Return the current PyOS_InputHook as a ctypes.c_void_p."""
79 79 return ctypes.c_void_p.in_dll(ctypes.pythonapi,"PyOS_InputHook")
80 80
81 81 def get_pyos_inputhook_as_func(self):
82 82 """Return the current PyOS_InputHook as a ctypes.PYFUNCYPE."""
83 83 return self.PYFUNC.in_dll(ctypes.pythonapi,"PyOS_InputHook")
84 84
85 85 def set_inputhook(self, callback):
86 86 """Set PyOS_InputHook to callback and return the previous one."""
87 87 self._callback = callback
88 88 self._callback_pyfunctype = self.PYFUNC(callback)
89 89 pyos_inputhook_ptr = self.get_pyos_inputhook()
90 90 original = self.get_pyos_inputhook_as_func()
91 91 pyos_inputhook_ptr.value = \
92 92 ctypes.cast(self._callback_pyfunctype, ctypes.c_void_p).value
93 93 self._installed = True
94 94 return original
95 95
96 96 def clear_inputhook(self, app=None):
97 97 """Set PyOS_InputHook to NULL and return the previous one.
98 98
99 99 Parameters
100 100 ----------
101 101 app : optional, ignored
102 102 This parameter is allowed only so that clear_inputhook() can be
103 103 called with a similar interface as all the ``enable_*`` methods. But
104 104 the actual value of the parameter is ignored. This uniform interface
105 105 makes it easier to have user-level entry points in the main IPython
106 106 app like :meth:`enable_gui`."""
107 107 pyos_inputhook_ptr = self.get_pyos_inputhook()
108 108 original = self.get_pyos_inputhook_as_func()
109 109 pyos_inputhook_ptr.value = ctypes.c_void_p(None).value
110 110 self._reset()
111 111 return original
112 112
113 113 def clear_app_refs(self, gui=None):
114 114 """Clear IPython's internal reference to an application instance.
115 115
116 116 Whenever we create an app for a user on qt4 or wx, we hold a
117 117 reference to the app. This is needed because in some cases bad things
118 118 can happen if a user doesn't hold a reference themselves. This
119 119 method is provided to clear the references we are holding.
120 120
121 121 Parameters
122 122 ----------
123 123 gui : None or str
124 124 If None, clear all app references. If ('wx', 'qt4') clear
125 125 the app for that toolkit. References are not held for gtk or tk
126 126 as those toolkits don't have the notion of an app.
127 127 """
128 128 if gui is None:
129 129 self._apps = {}
130 130 elif self._apps.has_key(gui):
131 131 del self._apps[gui]
132 132
133 133 def enable_wx(self, app=None):
134 134 """Enable event loop integration with wxPython.
135 135
136 136 Parameters
137 137 ----------
138 138 app : WX Application, optional.
139 139 Running application to use. If not given, we probe WX for an
140 140 existing application object, and create a new one if none is found.
141 141
142 142 Notes
143 143 -----
144 144 This methods sets the ``PyOS_InputHook`` for wxPython, which allows
145 145 the wxPython to integrate with terminal based applications like
146 146 IPython.
147 147
148 148 If ``app`` is not given we probe for an existing one, and return it if
149 149 found. If no existing app is found, we create an :class:`wx.App` as
150 150 follows::
151 151
152 152 import wx
153 153 app = wx.App(redirect=False, clearSigInt=False)
154 154 """
155 155 from IPython.lib.inputhookwx import inputhook_wx
156 156 self.set_inputhook(inputhook_wx)
157 157 self._current_gui = GUI_WX
158 158 import wx
159 159 if app is None:
160 160 app = wx.GetApp()
161 161 if app is None:
162 162 app = wx.App(redirect=False, clearSigInt=False)
163 163 app._in_event_loop = True
164 164 self._apps[GUI_WX] = app
165 165 return app
166 166
167 167 def disable_wx(self):
168 168 """Disable event loop integration with wxPython.
169 169
170 170 This merely sets PyOS_InputHook to NULL.
171 171 """
172 172 if self._apps.has_key(GUI_WX):
173 173 self._apps[GUI_WX]._in_event_loop = False
174 174 self.clear_inputhook()
175 175
176 176 def enable_qt4(self, app=None):
177 177 """Enable event loop integration with PyQt4.
178 178
179 179 Parameters
180 180 ----------
181 181 app : Qt Application, optional.
182 182 Running application to use. If not given, we probe Qt for an
183 183 existing application object, and create a new one if none is found.
184 184
185 185 Notes
186 186 -----
187 187 This methods sets the PyOS_InputHook for PyQt4, which allows
188 188 the PyQt4 to integrate with terminal based applications like
189 189 IPython.
190 190
191 191 If ``app`` is not given we probe for an existing one, and return it if
192 192 found. If no existing app is found, we create an :class:`QApplication`
193 193 as follows::
194 194
195 195 from PyQt4 import QtCore
196 196 app = QtGui.QApplication(sys.argv)
197 197 """
198 198 from IPython.external.qt_for_kernel import QtCore, QtGui
199 from IPython.core import ipapi
199 200
200 201 if app is None:
201 202 app = QtCore.QCoreApplication.instance()
202 203 if app is None:
203 204 app = QtGui.QApplication([" "])
204 205
205 # Always use the following input hook instead of PyQt4's
206 # default one, as it interacts better with readline packages
207 # (issue #481)
206 # Always use a custom input hook instead of PyQt4's default
207 # one, as it interacts better with readline packages (issue
208 # #481).
209
210 # Note that we can't let KeyboardInterrupt escape from that
211 # hook, (no exception can't be raised from within a ctypes
212 # python callback). We need to make a compromise: a trapped
213 # KeyboardInterrupt will prevent the input hook to re-enter
214 # the exec loop, until we start over with a new prompt line.
215 # This means one needs a double CTRL+C to get back to the
216 # prompt.
217
218 got_kbdint = [False]
219
220 def preprompthook_qt4(self):
221 got_kbdint[0] = False
222 ipapi.get().set_hook('pre_prompt_hook', preprompthook_qt4)
208 223
209 224 def inputhook_qt4():
210 225 try:
226 if got_kbdint[0]:
227 return 0
211 228 app.processEvents(QtCore.QEventLoop.AllEvents, 300)
212 229 if not stdin_ready():
213 230 timer = QtCore.QTimer()
214 231 timer.timeout.connect(app.quit)
215 232 while not stdin_ready():
216 233 timer.start(50)
217 234 app.exec_()
218 235 timer.stop()
219 236 except KeyboardInterrupt:
220 pass
237 got_kbdint[0] = True
238 print("\n(event loop interrupted - "
239 "hit CTRL+C again to clear the prompt)")
221 240 return 0
222 241 self.set_inputhook(inputhook_qt4)
223 242
224 243 self._current_gui = GUI_QT4
225 244 app._in_event_loop = True
226 245 self._apps[GUI_QT4] = app
227 246 return app
228 247
229 248 def disable_qt4(self):
230 249 """Disable event loop integration with PyQt4.
231 250
232 251 This merely sets PyOS_InputHook to NULL.
233 252 """
234 253 if self._apps.has_key(GUI_QT4):
235 254 self._apps[GUI_QT4]._in_event_loop = False
236 255 self.clear_inputhook()
237 256
238 257 def enable_gtk(self, app=None):
239 258 """Enable event loop integration with PyGTK.
240 259
241 260 Parameters
242 261 ----------
243 262 app : ignored
244 263 Ignored, it's only a placeholder to keep the call signature of all
245 264 gui activation methods consistent, which simplifies the logic of
246 265 supporting magics.
247 266
248 267 Notes
249 268 -----
250 269 This methods sets the PyOS_InputHook for PyGTK, which allows
251 270 the PyGTK to integrate with terminal based applications like
252 271 IPython.
253 272 """
254 273 import gtk
255 274 try:
256 275 gtk.set_interactive(True)
257 276 self._current_gui = GUI_GTK
258 277 except AttributeError:
259 278 # For older versions of gtk, use our own ctypes version
260 279 from IPython.lib.inputhookgtk import inputhook_gtk
261 280 self.set_inputhook(inputhook_gtk)
262 281 self._current_gui = GUI_GTK
263 282
264 283 def disable_gtk(self):
265 284 """Disable event loop integration with PyGTK.
266 285
267 286 This merely sets PyOS_InputHook to NULL.
268 287 """
269 288 self.clear_inputhook()
270 289
271 290 def enable_tk(self, app=None):
272 291 """Enable event loop integration with Tk.
273 292
274 293 Parameters
275 294 ----------
276 295 app : toplevel :class:`Tkinter.Tk` widget, optional.
277 296 Running toplevel widget to use. If not given, we probe Tk for an
278 297 existing one, and create a new one if none is found.
279 298
280 299 Notes
281 300 -----
282 301 If you have already created a :class:`Tkinter.Tk` object, the only
283 302 thing done by this method is to register with the
284 303 :class:`InputHookManager`, since creating that object automatically
285 304 sets ``PyOS_InputHook``.
286 305 """
287 306 self._current_gui = GUI_TK
288 307 if app is None:
289 308 import Tkinter
290 309 app = Tkinter.Tk()
291 310 app.withdraw()
292 311 self._apps[GUI_TK] = app
293 312 return app
294 313
295 314 def disable_tk(self):
296 315 """Disable event loop integration with Tkinter.
297 316
298 317 This merely sets PyOS_InputHook to NULL.
299 318 """
300 319 self.clear_inputhook()
301 320
302 321
303 322 def enable_glut(self, app=None):
304 323 """ Enable event loop integration with GLUT.
305 324
306 325 Parameters
307 326 ----------
308 327
309 328 app : ignored
310 329 Ignored, it's only a placeholder to keep the call signature of all
311 330 gui activation methods consistent, which simplifies the logic of
312 331 supporting magics.
313 332
314 333 Notes
315 334 -----
316 335
317 336 This methods sets the PyOS_InputHook for GLUT, which allows the GLUT to
318 337 integrate with terminal based applications like IPython. Due to GLUT
319 338 limitations, it is currently not possible to start the event loop
320 339 without first creating a window. You should thus not create another
321 340 window but use instead the created one. See 'gui-glut.py' in the
322 341 docs/examples/lib directory.
323 342
324 343 The default screen mode is set to:
325 344 glut.GLUT_DOUBLE | glut.GLUT_RGBA | glut.GLUT_DEPTH
326 345 """
327 346
328 347 import OpenGL.GLUT as glut
329 348 from IPython.lib.inputhookglut import glut_display_mode, \
330 349 glut_close, glut_display, \
331 350 glut_idle, inputhook_glut
332 351
333 352 if not self._apps.has_key( GUI_GLUT ):
334 353 glut.glutInit( sys.argv )
335 354 glut.glutInitDisplayMode( glut_display_mode )
336 355 # This is specific to freeglut
337 356 if bool(glut.glutSetOption):
338 357 glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE,
339 358 glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS )
340 359 glut.glutCreateWindow( sys.argv[0] )
341 360 glut.glutReshapeWindow( 1, 1 )
342 361 glut.glutHideWindow( )
343 362 glut.glutWMCloseFunc( glut_close )
344 363 glut.glutDisplayFunc( glut_display )
345 364 glut.glutIdleFunc( glut_idle )
346 365 else:
347 366 glut.glutWMCloseFunc( glut_close )
348 367 glut.glutDisplayFunc( glut_display )
349 368 glut.glutIdleFunc( glut_idle)
350 369 self.set_inputhook( inputhook_glut )
351 370 self._current_gui = GUI_GLUT
352 371 self._apps[GUI_GLUT] = True
353 372
354 373
355 374 def disable_glut(self):
356 375 """Disable event loop integration with glut.
357 376
358 377 This sets PyOS_InputHook to NULL and set the display function to a
359 378 dummy one and set the timer to a dummy timer that will be triggered
360 379 very far in the future.
361 380 """
362 381 import OpenGL.GLUT as glut
363 382 from glut_support import glutMainLoopEvent
364 383
365 384 glut.glutHideWindow() # This is an event to be processed below
366 385 glutMainLoopEvent()
367 386 self.clear_inputhook()
368 387
369 388 def enable_pyglet(self, app=None):
370 389 """Enable event loop integration with pyglet.
371 390
372 391 Parameters
373 392 ----------
374 393 app : ignored
375 394 Ignored, it's only a placeholder to keep the call signature of all
376 395 gui activation methods consistent, which simplifies the logic of
377 396 supporting magics.
378 397
379 398 Notes
380 399 -----
381 400 This methods sets the ``PyOS_InputHook`` for pyglet, which allows
382 401 pyglet to integrate with terminal based applications like
383 402 IPython.
384 403
385 404 """
386 405 import pyglet
387 406 from IPython.lib.inputhookpyglet import inputhook_pyglet
388 407 self.set_inputhook(inputhook_pyglet)
389 408 self._current_gui = GUI_PYGLET
390 409 return app
391 410
392 411 def disable_pyglet(self):
393 412 """Disable event loop integration with pyglet.
394 413
395 414 This merely sets PyOS_InputHook to NULL.
396 415 """
397 416 self.clear_inputhook()
398 417
399 418 def current_gui(self):
400 419 """Return a string indicating the currently active GUI or None."""
401 420 return self._current_gui
402 421
403 422 inputhook_manager = InputHookManager()
404 423
405 424 enable_wx = inputhook_manager.enable_wx
406 425 disable_wx = inputhook_manager.disable_wx
407 426 enable_qt4 = inputhook_manager.enable_qt4
408 427 disable_qt4 = inputhook_manager.disable_qt4
409 428 enable_gtk = inputhook_manager.enable_gtk
410 429 disable_gtk = inputhook_manager.disable_gtk
411 430 enable_tk = inputhook_manager.enable_tk
412 431 disable_tk = inputhook_manager.disable_tk
413 432 enable_glut = inputhook_manager.enable_glut
414 433 disable_glut = inputhook_manager.disable_glut
415 434 enable_pyglet = inputhook_manager.enable_pyglet
416 435 disable_pyglet = inputhook_manager.disable_pyglet
417 436 clear_inputhook = inputhook_manager.clear_inputhook
418 437 set_inputhook = inputhook_manager.set_inputhook
419 438 current_gui = inputhook_manager.current_gui
420 439 clear_app_refs = inputhook_manager.clear_app_refs
421 440
422 441
423 442 # Convenience function to switch amongst them
424 443 def enable_gui(gui=None, app=None):
425 444 """Switch amongst GUI input hooks by name.
426 445
427 446 This is just a utility wrapper around the methods of the InputHookManager
428 447 object.
429 448
430 449 Parameters
431 450 ----------
432 451 gui : optional, string or None
433 452 If None, clears input hook, otherwise it must be one of the recognized
434 453 GUI names (see ``GUI_*`` constants in module).
435 454
436 455 app : optional, existing application object.
437 456 For toolkits that have the concept of a global app, you can supply an
438 457 existing one. If not given, the toolkit will be probed for one, and if
439 458 none is found, a new one will be created. Note that GTK does not have
440 459 this concept, and passing an app if `gui`=="GTK" will raise an error.
441 460
442 461 Returns
443 462 -------
444 463 The output of the underlying gui switch routine, typically the actual
445 464 PyOS_InputHook wrapper object or the GUI toolkit app created, if there was
446 465 one.
447 466 """
448 467 guis = {None: clear_inputhook,
449 468 GUI_OSX: lambda app=False: None,
450 469 GUI_TK: enable_tk,
451 470 GUI_GTK: enable_gtk,
452 471 GUI_WX: enable_wx,
453 472 GUI_QT: enable_qt4, # qt3 not supported
454 473 GUI_QT4: enable_qt4,
455 474 GUI_GLUT: enable_glut,
456 475 GUI_PYGLET: enable_pyglet,
457 476 }
458 477 try:
459 478 gui_hook = guis[gui]
460 479 except KeyError:
461 480 e = "Invalid GUI request %r, valid ones are:%s" % (gui, guis.keys())
462 481 raise ValueError(e)
463 482 return gui_hook(app)
464 483
General Comments 0
You need to be logged in to leave comments. Login now