1# -*- coding: utf-8 -*- 2""" 3Qt5's inputhook support function 4 5Author: Christian Boos 6""" 7 8#----------------------------------------------------------------------------- 9# Copyright (C) 2011 The IPython Development Team 10# 11# Distributed under the terms of the BSD License. The full license is in 12# the file COPYING, distributed as part of this software. 13#----------------------------------------------------------------------------- 14 15#----------------------------------------------------------------------------- 16# Imports 17#----------------------------------------------------------------------------- 18 19import os 20import signal 21 22import threading 23 24 25from pydev_ipython.qt_for_kernel import QtCore, QtGui 26from pydev_ipython.inputhook import allow_CTRL_C, ignore_CTRL_C, stdin_ready 27 28# To minimise future merging complexity, rather than edit the entire code base below 29# we fake InteractiveShell here 30class InteractiveShell: 31 _instance = None 32 @classmethod 33 def instance(cls): 34 if cls._instance is None: 35 cls._instance = cls() 36 return cls._instance 37 def set_hook(self, *args, **kwargs): 38 # We don't consider the pre_prompt_hook because we don't have 39 # KeyboardInterrupts to consider since we are running under PyDev 40 pass 41 42 43#----------------------------------------------------------------------------- 44# Module Globals 45#----------------------------------------------------------------------------- 46 47got_kbdint = False 48sigint_timer = None 49 50#----------------------------------------------------------------------------- 51# Code 52#----------------------------------------------------------------------------- 53 54def create_inputhook_qt5(mgr, app=None): 55 """Create an input hook for running the Qt5 application event loop. 56 57 Parameters 58 ---------- 59 mgr : an InputHookManager 60 61 app : Qt Application, optional. 62 Running application to use. If not given, we probe Qt for an 63 existing application object, and create a new one if none is found. 64 65 Returns 66 ------- 67 A pair consisting of a Qt Application (either the one given or the 68 one found or created) and a inputhook. 69 70 Notes 71 ----- 72 We use a custom input hook instead of PyQt5's default one, as it 73 interacts better with the readline packages (issue #481). 74 75 The inputhook function works in tandem with a 'pre_prompt_hook' 76 which automatically restores the hook as an inputhook in case the 77 latter has been temporarily disabled after having intercepted a 78 KeyboardInterrupt. 79 """ 80 81 if app is None: 82 app = QtCore.QCoreApplication.instance() 83 if app is None: 84 from PyQt5 import QtWidgets 85 app = QtWidgets.QApplication([" "]) 86 87 # Re-use previously created inputhook if any 88 ip = InteractiveShell.instance() 89 if hasattr(ip, '_inputhook_qt5'): 90 return app, ip._inputhook_qt5 91 92 # Otherwise create the inputhook_qt5/preprompthook_qt5 pair of 93 # hooks (they both share the got_kbdint flag) 94 95 def inputhook_qt5(): 96 """PyOS_InputHook python hook for Qt5. 97 98 Process pending Qt events and if there's no pending keyboard 99 input, spend a short slice of time (50ms) running the Qt event 100 loop. 101 102 As a Python ctypes callback can't raise an exception, we catch 103 the KeyboardInterrupt and temporarily deactivate the hook, 104 which will let a *second* CTRL+C be processed normally and go 105 back to a clean prompt line. 106 """ 107 try: 108 allow_CTRL_C() 109 app = QtCore.QCoreApplication.instance() 110 if not app: # shouldn't happen, but safer if it happens anyway... 111 return 0 112 app.processEvents(QtCore.QEventLoop.AllEvents, 300) 113 if not stdin_ready(): 114 # Generally a program would run QCoreApplication::exec() 115 # from main() to enter and process the Qt event loop until 116 # quit() or exit() is called and the program terminates. 117 # 118 # For our input hook integration, we need to repeatedly 119 # enter and process the Qt event loop for only a short 120 # amount of time (say 50ms) to ensure that Python stays 121 # responsive to other user inputs. 122 # 123 # A naive approach would be to repeatedly call 124 # QCoreApplication::exec(), using a timer to quit after a 125 # short amount of time. Unfortunately, QCoreApplication 126 # emits an aboutToQuit signal before stopping, which has 127 # the undesirable effect of closing all modal windows. 128 # 129 # To work around this problem, we instead create a 130 # QEventLoop and call QEventLoop::exec(). Other than 131 # setting some state variables which do not seem to be 132 # used anywhere, the only thing QCoreApplication adds is 133 # the aboutToQuit signal which is precisely what we are 134 # trying to avoid. 135 timer = QtCore.QTimer() 136 event_loop = QtCore.QEventLoop() 137 timer.timeout.connect(event_loop.quit) 138 while not stdin_ready(): 139 timer.start(50) 140 event_loop.exec_() 141 timer.stop() 142 except KeyboardInterrupt: 143 global got_kbdint, sigint_timer 144 145 ignore_CTRL_C() 146 got_kbdint = True 147 mgr.clear_inputhook() 148 149 # This generates a second SIGINT so the user doesn't have to 150 # press CTRL+C twice to get a clean prompt. 151 # 152 # Since we can't catch the resulting KeyboardInterrupt here 153 # (because this is a ctypes callback), we use a timer to 154 # generate the SIGINT after we leave this callback. 155 # 156 # Unfortunately this doesn't work on Windows (SIGINT kills 157 # Python and CTRL_C_EVENT doesn't work). 158 if(os.name == 'posix'): 159 pid = os.getpid() 160 if(not sigint_timer): 161 sigint_timer = threading.Timer(.01, os.kill, 162 args=[pid, signal.SIGINT] ) 163 sigint_timer.start() 164 else: 165 print("\nKeyboardInterrupt - Ctrl-C again for new prompt") 166 167 168 except: # NO exceptions are allowed to escape from a ctypes callback 169 ignore_CTRL_C() 170 from traceback import print_exc 171 print_exc() 172 print("Got exception from inputhook_qt5, unregistering.") 173 mgr.clear_inputhook() 174 finally: 175 allow_CTRL_C() 176 return 0 177 178 def preprompthook_qt5(ishell): 179 """'pre_prompt_hook' used to restore the Qt5 input hook 180 181 (in case the latter was temporarily deactivated after a 182 CTRL+C) 183 """ 184 global got_kbdint, sigint_timer 185 186 if(sigint_timer): 187 sigint_timer.cancel() 188 sigint_timer = None 189 190 if got_kbdint: 191 mgr.set_inputhook(inputhook_qt5) 192 got_kbdint = False 193 194 ip._inputhook_qt5 = inputhook_qt5 195 ip.set_hook('pre_prompt_hook', preprompthook_qt5) 196 197 return app, inputhook_qt5 198