1# -*- coding: utf-8 -*-
2"""
3Qt4'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_qt4(mgr, app=None):
55    """Create an input hook for running the Qt4 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 PyQt4'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            app = QtGui.QApplication([" "])
85
86    # Re-use previously created inputhook if any
87    ip = InteractiveShell.instance()
88    if hasattr(ip, '_inputhook_qt4'):
89        return app, ip._inputhook_qt4
90
91    # Otherwise create the inputhook_qt4/preprompthook_qt4 pair of
92    # hooks (they both share the got_kbdint flag)
93
94    def inputhook_qt4():
95        """PyOS_InputHook python hook for Qt4.
96
97        Process pending Qt events and if there's no pending keyboard
98        input, spend a short slice of time (50ms) running the Qt event
99        loop.
100
101        As a Python ctypes callback can't raise an exception, we catch
102        the KeyboardInterrupt and temporarily deactivate the hook,
103        which will let a *second* CTRL+C be processed normally and go
104        back to a clean prompt line.
105        """
106        try:
107            allow_CTRL_C()
108            app = QtCore.QCoreApplication.instance()
109            if not app: # shouldn't happen, but safer if it happens anyway...
110                return 0
111            app.processEvents(QtCore.QEventLoop.AllEvents, 300)
112            if not stdin_ready():
113                # Generally a program would run QCoreApplication::exec()
114                # from main() to enter and process the Qt event loop until
115                # quit() or exit() is called and the program terminates.
116                #
117                # For our input hook integration, we need to repeatedly
118                # enter and process the Qt event loop for only a short
119                # amount of time (say 50ms) to ensure that Python stays
120                # responsive to other user inputs.
121                #
122                # A naive approach would be to repeatedly call
123                # QCoreApplication::exec(), using a timer to quit after a
124                # short amount of time. Unfortunately, QCoreApplication
125                # emits an aboutToQuit signal before stopping, which has
126                # the undesirable effect of closing all modal windows.
127                #
128                # To work around this problem, we instead create a
129                # QEventLoop and call QEventLoop::exec(). Other than
130                # setting some state variables which do not seem to be
131                # used anywhere, the only thing QCoreApplication adds is
132                # the aboutToQuit signal which is precisely what we are
133                # trying to avoid.
134                timer = QtCore.QTimer()
135                event_loop = QtCore.QEventLoop()
136                timer.timeout.connect(event_loop.quit)
137                while not stdin_ready():
138                    timer.start(50)
139                    event_loop.exec_()
140                    timer.stop()
141        except KeyboardInterrupt:
142            global got_kbdint, sigint_timer
143
144            ignore_CTRL_C()
145            got_kbdint = True
146            mgr.clear_inputhook()
147
148            # This generates a second SIGINT so the user doesn't have to
149            # press CTRL+C twice to get a clean prompt.
150            #
151            # Since we can't catch the resulting KeyboardInterrupt here
152            # (because this is a ctypes callback), we use a timer to
153            # generate the SIGINT after we leave this callback.
154            #
155            # Unfortunately this doesn't work on Windows (SIGINT kills
156            # Python and CTRL_C_EVENT doesn't work).
157            if(os.name == 'posix'):
158                pid = os.getpid()
159                if(not sigint_timer):
160                    sigint_timer = threading.Timer(.01, os.kill,
161                                         args=[pid, signal.SIGINT] )
162                    sigint_timer.start()
163            else:
164                print("\nKeyboardInterrupt - Ctrl-C again for new prompt")
165
166
167        except: # NO exceptions are allowed to escape from a ctypes callback
168            ignore_CTRL_C()
169            from traceback import print_exc
170            print_exc()
171            print("Got exception from inputhook_qt4, unregistering.")
172            mgr.clear_inputhook()
173        finally:
174            allow_CTRL_C()
175        return 0
176
177    def preprompthook_qt4(ishell):
178        """'pre_prompt_hook' used to restore the Qt4 input hook
179
180        (in case the latter was temporarily deactivated after a
181        CTRL+C)
182        """
183        global got_kbdint, sigint_timer
184
185        if(sigint_timer):
186            sigint_timer.cancel()
187            sigint_timer = None
188
189        if got_kbdint:
190            mgr.set_inputhook(inputhook_qt4)
191        got_kbdint = False
192
193    ip._inputhook_qt4 = inputhook_qt4
194    ip.set_hook('pre_prompt_hook', preprompthook_qt4)
195
196    return app, inputhook_qt4
197