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