From 1f7e9d752bea73a8f4e4542f57e846593aa1cacc 2011-10-28 22:24:42 From: Fernando Perez Date: 2011-10-28 22:24:42 Subject: [PATCH] Merge pull request #815 from cboos/issue481-qt4-input-hook Fix #481 using custom qt4 input hook. Various other cleanups to inputhook libraries. --- diff --git a/IPython/lib/inputhook.py b/IPython/lib/inputhook.py index 085a271..7e9b954 100644 --- a/IPython/lib/inputhook.py +++ b/IPython/lib/inputhook.py @@ -15,6 +15,7 @@ Inputhook management for GUI event loop integration. #----------------------------------------------------------------------------- import ctypes +import os import sys import warnings @@ -31,11 +32,58 @@ GUI_TK = 'tk' GUI_OSX = 'osx' GUI_GLUT = 'glut' GUI_PYGLET = 'pyglet' +GUI_NONE = 'none' # i.e. disable #----------------------------------------------------------------------------- -# Utility classes +# Utilities #----------------------------------------------------------------------------- +def _stdin_ready_posix(): + """Return True if there's something to read on stdin (posix version).""" + infds, outfds, erfds = select.select([sys.stdin],[],[],0) + return bool(infds) + +def _stdin_ready_nt(): + """Return True if there's something to read on stdin (nt version).""" + return msvcrt.kbhit() + +def _stdin_ready_other(): + """Return True, assuming there's something to read on stdin.""" + return True # + + +def _ignore_CTRL_C_posix(): + """Ignore CTRL+C (SIGINT).""" + signal.signal(signal.SIGINT, signal.SIG_IGN) + +def _allow_CTRL_C_posix(): + """Take CTRL+C into account (SIGINT).""" + signal.signal(signal.SIGINT, signal.default_int_handler) + +def _ignore_CTRL_C_other(): + """Ignore CTRL+C (not implemented).""" + pass + +def _allow_CTRL_C_other(): + """Take CTRL+C into account (not implemented).""" + pass + +if os.name == 'posix': + import select + import signal + stdin_ready = _stdin_ready_posix + ignore_CTRL_C = _ignore_CTRL_C_posix + allow_CTRL_C = _allow_CTRL_C_posix +elif os.name == 'nt': + import msvcrt + stdin_ready = _stdin_ready_nt + ignore_CTRL_C = _ignore_CTRL_C_other + allow_CTRL_C = _allow_CTRL_C_other +else: + stdin_ready = _stdin_ready_other + ignore_CTRL_C = _ignore_CTRL_C_other + allow_CTRL_C = _allow_CTRL_C_other + #----------------------------------------------------------------------------- # Main InputHookManager class @@ -70,6 +118,11 @@ class InputHookManager(object): def set_inputhook(self, callback): """Set PyOS_InputHook to callback and return the previous one.""" + # On platforms with 'readline' support, it's all too likely to + # have a KeyboardInterrupt signal delivered *even before* an + # initial ``try:`` clause in the callback can be executed, so + # we need to disable CTRL+C in this situation. + ignore_CTRL_C() self._callback = callback self._callback_pyfunctype = self.PYFUNC(callback) pyos_inputhook_ptr = self.get_pyos_inputhook() @@ -93,6 +146,7 @@ class InputHookManager(object): pyos_inputhook_ptr = self.get_pyos_inputhook() original = self.get_pyos_inputhook_as_func() pyos_inputhook_ptr.value = ctypes.c_void_p(None).value + allow_CTRL_C() self._reset() return original @@ -181,33 +235,11 @@ class InputHookManager(object): from PyQt4 import QtCore app = QtGui.QApplication(sys.argv) """ - from IPython.external.qt_for_kernel import QtCore, QtGui - - if 'pyreadline' in sys.modules: - # see IPython GitHub Issue #281 for more info on this issue - # Similar intermittent behavior has been reported on OSX, - # but not consistently reproducible - warnings.warn("""PyReadline's inputhook can conflict with Qt, causing delays - in interactive input. If you do see this issue, we recommend using another GUI - toolkit if you can, or disable readline with the configuration option - 'TerminalInteractiveShell.readline_use=False', specified in a config file or - at the command-line""", - RuntimeWarning) - - # PyQt4 has had this since 4.3.1. In version 4.2, PyOS_InputHook - # was set when QtCore was imported, but if it ever got removed, - # you couldn't reset it. For earlier versions we can - # probably implement a ctypes version. - try: - QtCore.pyqtRestoreInputHook() - except AttributeError: - pass + from IPython.lib.inputhookqt4 import create_inputhook_qt4 + app, inputhook_qt4 = create_inputhook_qt4(self, app) + self.set_inputhook(inputhook_qt4) self._current_gui = GUI_QT4 - if app is None: - app = QtCore.QCoreApplication.instance() - if app is None: - app = QtGui.QApplication([" "]) app._in_event_loop = True self._apps[GUI_QT4] = app return app @@ -416,8 +448,8 @@ def enable_gui(gui=None, app=None): Parameters ---------- gui : optional, string or None - If None, clears input hook, otherwise it must be one of the recognized - GUI names (see ``GUI_*`` constants in module). + If None (or 'none'), clears input hook, otherwise it must be one + of the recognized GUI names (see ``GUI_*`` constants in module). app : optional, existing application object. For toolkits that have the concept of a global app, you can supply an @@ -432,6 +464,7 @@ def enable_gui(gui=None, app=None): one. """ guis = {None: clear_inputhook, + GUI_NONE: clear_inputhook, GUI_OSX: lambda app=False: None, GUI_TK: enable_tk, GUI_GTK: enable_gtk, diff --git a/IPython/lib/inputhookqt4.py b/IPython/lib/inputhookqt4.py new file mode 100644 index 0000000..0c0afa8 --- /dev/null +++ b/IPython/lib/inputhookqt4.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +""" +Qt4's inputhook support function + +Author: Christian Boos +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2011 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +from IPython.core.interactiveshell import InteractiveShell +from IPython.external.qt_for_kernel import QtCore, QtGui +from IPython.lib.inputhook import allow_CTRL_C, ignore_CTRL_C, stdin_ready + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- + +def create_inputhook_qt4(mgr, app=None): + """Create an input hook for running the Qt4 application event loop. + + Parameters + ---------- + mgr : an InputHookManager + + app : Qt Application, optional. + Running application to use. If not given, we probe Qt for an + existing application object, and create a new one if none is found. + + Returns + ------- + A pair consisting of a Qt Application (either the one given or the + one found or created) and a inputhook. + + Notes + ----- + We use a custom input hook instead of PyQt4's default one, as it + interacts better with the readline packages (issue #481). + + The inputhook function works in tandem with a 'pre_prompt_hook' + which automatically restores the hook as an inputhook in case the + latter has been temporarily disabled after having intercepted a + KeyboardInterrupt. + """ + + if app is None: + app = QtCore.QCoreApplication.instance() + if app is None: + app = QtGui.QApplication([" "]) + + # Re-use previously created inputhook if any + ip = InteractiveShell.instance() + if hasattr(ip, '_inputhook_qt4'): + return app, ip._inputhook_qt4 + + # Otherwise create the inputhook_qt4/preprompthook_qt4 pair of + # hooks (they both share the got_kbdint flag) + + got_kbdint = [False] + + def inputhook_qt4(): + """PyOS_InputHook python hook for Qt4. + + Process pending Qt events and if there's no pending keyboard + input, spend a short slice of time (50ms) running the Qt event + loop. + + As a Python ctypes callback can't raise an exception, we catch + the KeyboardInterrupt and temporarily deactivate the hook, + which will let a *second* CTRL+C be processed normally and go + back to a clean prompt line. + """ + try: + allow_CTRL_C() + app = QtCore.QCoreApplication.instance() + if not app: # shouldn't happen, but safer if it happens anyway... + return 0 + app.processEvents(QtCore.QEventLoop.AllEvents, 300) + if not stdin_ready(): + timer = QtCore.QTimer() + timer.timeout.connect(app.quit) + while not stdin_ready(): + timer.start(50) + app.exec_() + timer.stop() + ignore_CTRL_C() + except KeyboardInterrupt: + ignore_CTRL_C() + got_kbdint[0] = True + print("\nKeyboardInterrupt - qt4 event loop interrupted!" + "\n * hit CTRL+C again to clear the prompt" + "\n * use '%gui none' to disable the event loop" + " permanently" + "\n and '%gui qt4' to re-enable it later") + mgr.clear_inputhook() + except: # NO exceptions are allowed to escape from a ctypes callback + mgr.clear_inputhook() + from traceback import print_exc + print_exc() + print("Got exception from inputhook_qt4, unregistering.") + return 0 + + def preprompthook_qt4(ishell): + """'pre_prompt_hook' used to restore the Qt4 input hook + + (in case the latter was temporarily deactivated after a + CTRL+C) + """ + if got_kbdint[0]: + mgr.set_inputhook(inputhook_qt4) + got_kbdint[0] = False + + ip._inputhook_qt4 = inputhook_qt4 + ip.set_hook('pre_prompt_hook', preprompthook_qt4) + + return app, inputhook_qt4 diff --git a/IPython/lib/inputhookwx.py b/IPython/lib/inputhookwx.py index 8e3d25b..4b9bd02 100644 --- a/IPython/lib/inputhookwx.py +++ b/IPython/lib/inputhookwx.py @@ -24,26 +24,13 @@ import time from timeit import default_timer as clock import wx -if os.name == 'posix': - import select -elif sys.platform == 'win32': - import msvcrt +from IPython.lib.inputhook import stdin_ready + #----------------------------------------------------------------------------- # Code #----------------------------------------------------------------------------- -def stdin_ready(): - if os.name == 'posix': - infds, outfds, erfds = select.select([sys.stdin],[],[],0) - if infds: - return True - else: - return False - elif sys.platform == 'win32': - return msvcrt.kbhit() - - def inputhook_wx1(): """Run the wx event loop by processing pending events only.