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 PyQt5 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 # Warning: calling event_loop.exec_() can lead to hangs in REPL on mscOS PY-31931 141 # Replacing it with event_loop.processEvents() fixes the issue, but leads to high CPU load on every os PY-42688 142 event_loop.exec_() 143 timer.stop() 144 except KeyboardInterrupt: 145 global got_kbdint, sigint_timer 146 147 ignore_CTRL_C() 148 got_kbdint = True 149 mgr.clear_inputhook() 150 151 # This generates a second SIGINT so the user doesn't have to 152 # press CTRL+C twice to get a clean prompt. 153 # 154 # Since we can't catch the resulting KeyboardInterrupt here 155 # (because this is a ctypes callback), we use a timer to 156 # generate the SIGINT after we leave this callback. 157 # 158 # Unfortunately this doesn't work on Windows (SIGINT kills 159 # Python and CTRL_C_EVENT doesn't work). 160 if(os.name == 'posix'): 161 pid = os.getpid() 162 if(not sigint_timer): 163 sigint_timer = threading.Timer(.01, os.kill, 164 args=[pid, signal.SIGINT] ) 165 sigint_timer.start() 166 else: 167 print("\nKeyboardInterrupt - Ctrl-C again for new prompt") 168 169 170 except: # NO exceptions are allowed to escape from a ctypes callback 171 ignore_CTRL_C() 172 from traceback import print_exc 173 print_exc() 174 print("Got exception from inputhook_qt5, unregistering.") 175 mgr.clear_inputhook() 176 finally: 177 allow_CTRL_C() 178 return 0 179 180 def preprompthook_qt5(ishell): 181 """'pre_prompt_hook' used to restore the Qt5 input hook 182 183 (in case the latter was temporarily deactivated after a 184 CTRL+C) 185 """ 186 global got_kbdint, sigint_timer 187 188 if(sigint_timer): 189 sigint_timer.cancel() 190 sigint_timer = None 191 192 if got_kbdint: 193 mgr.set_inputhook(inputhook_qt5) 194 got_kbdint = False 195 196 ip._inputhook_qt5 = inputhook_qt5 197 ip.set_hook('pre_prompt_hook', preprompthook_qt5) 198 199 return app, inputhook_qt5 200