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