diff --git a/python/helpers/pydev/pydev_ipython/inputhook.py b/python/helpers/pydev/pydev_ipython/inputhook.py index 4d4db3454440..58ae93118d08 100644 --- a/python/helpers/pydev/pydev_ipython/inputhook.py +++ b/python/helpers/pydev/pydev_ipython/inputhook.py @@ -25,6 +25,7 @@ import select GUI_WX = 'wx' GUI_QT = 'qt' GUI_QT4 = 'qt4' +GUI_QT5 = 'qt5' GUI_GTK = 'gtk' GUI_TK = 'tk' GUI_OSX = 'osx' @@ -209,6 +210,21 @@ class InputHookManager(object): self._apps[GUI_QT4]._in_event_loop = False self.clear_inputhook() + def enable_qt5(self, app=None): + from pydev_ipython.inputhookqt5 import create_inputhook_qt5 + app, inputhook_qt5 = create_inputhook_qt5(self, app) + self.set_inputhook(inputhook_qt5) + + self._current_gui = GUI_QT5 + app._in_event_loop = True + self._apps[GUI_QT5] = app + return app + + def disable_qt5(self): + if GUI_QT5 in self._apps: + self._apps[GUI_QT5]._in_event_loop = False + self.clear_inputhook() + def enable_gtk(self, app=None): """Enable event loop integration with PyGTK. @@ -302,8 +318,8 @@ class InputHookManager(object): import OpenGL.GLUT as glut # @UnresolvedImport from pydev_ipython.inputhookglut import glut_display_mode, \ - glut_close, glut_display, \ - glut_idle, inputhook_glut + glut_close, glut_display, \ + glut_idle, inputhook_glut if GUI_GLUT not in self._apps: glut.glutInit(sys.argv) @@ -311,7 +327,7 @@ class InputHookManager(object): # This is specific to freeglut if bool(glut.glutSetOption): glut.glutSetOption(glut.GLUT_ACTION_ON_WINDOW_CLOSE, - glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS) + glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS) glut.glutCreateWindow(sys.argv[0]) glut.glutReshapeWindow(1, 1) glut.glutHideWindow() @@ -434,6 +450,8 @@ enable_wx = inputhook_manager.enable_wx disable_wx = inputhook_manager.disable_wx enable_qt4 = inputhook_manager.enable_qt4 disable_qt4 = inputhook_manager.disable_qt4 +enable_qt5 = inputhook_manager.enable_qt5 +disable_qt5 = inputhook_manager.disable_qt5 enable_gtk = inputhook_manager.enable_gtk disable_gtk = inputhook_manager.disable_gtk enable_tk = inputhook_manager.enable_tk @@ -494,6 +512,7 @@ def enable_gui(gui=None, app=None): GUI_WX: enable_wx, GUI_QT: enable_qt4, # qt3 not supported GUI_QT4: enable_qt4, + GUI_QT5: enable_qt5, GUI_GLUT: enable_glut, GUI_PYGLET: enable_pyglet, GUI_GTK3: enable_gtk3, @@ -512,6 +531,7 @@ __all__ = [ "GUI_WX", "GUI_QT", "GUI_QT4", + "GUI_QT5", "GUI_GTK", "GUI_TK", "GUI_OSX", @@ -532,6 +552,8 @@ __all__ = [ "disable_wx", "enable_qt4", "disable_qt4", + "enable_qt5", + "disable_qt5", "enable_gtk", "disable_gtk", "enable_tk", diff --git a/python/helpers/pydev/pydev_ipython/inputhookqt5.py b/python/helpers/pydev/pydev_ipython/inputhookqt5.py new file mode 100644 index 000000000000..77b938b478cf --- /dev/null +++ b/python/helpers/pydev/pydev_ipython/inputhookqt5.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +""" +Qt5'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 +#----------------------------------------------------------------------------- + +import os +import signal + +import threading + + +from pydev_ipython.qt_for_kernel import QtCore, QtGui +from pydev_ipython.inputhook import allow_CTRL_C, ignore_CTRL_C, stdin_ready + +# To minimise future merging complexity, rather than edit the entire code base below +# we fake InteractiveShell here +class InteractiveShell: + _instance = None + @classmethod + def instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + def set_hook(self, *args, **kwargs): + # We don't consider the pre_prompt_hook because we don't have + # KeyboardInterrupts to consider since we are running under PyDev + pass + + +#----------------------------------------------------------------------------- +# Module Globals +#----------------------------------------------------------------------------- + +got_kbdint = False +sigint_timer = None + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- + +def create_inputhook_qt5(mgr, app=None): + """Create an input hook for running the Qt5 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 PyQt5'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: + from PyQt5 import QtWidgets + app = QtWidgets.QApplication([" "]) + + # Re-use previously created inputhook if any + ip = InteractiveShell.instance() + if hasattr(ip, '_inputhook_qt5'): + return app, ip._inputhook_qt5 + + # Otherwise create the inputhook_qt5/preprompthook_qt5 pair of + # hooks (they both share the got_kbdint flag) + + def inputhook_qt5(): + """PyOS_InputHook python hook for Qt5. + + 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(): + # Generally a program would run QCoreApplication::exec() + # from main() to enter and process the Qt event loop until + # quit() or exit() is called and the program terminates. + # + # For our input hook integration, we need to repeatedly + # enter and process the Qt event loop for only a short + # amount of time (say 50ms) to ensure that Python stays + # responsive to other user inputs. + # + # A naive approach would be to repeatedly call + # QCoreApplication::exec(), using a timer to quit after a + # short amount of time. Unfortunately, QCoreApplication + # emits an aboutToQuit signal before stopping, which has + # the undesirable effect of closing all modal windows. + # + # To work around this problem, we instead create a + # QEventLoop and call QEventLoop::exec(). Other than + # setting some state variables which do not seem to be + # used anywhere, the only thing QCoreApplication adds is + # the aboutToQuit signal which is precisely what we are + # trying to avoid. + timer = QtCore.QTimer() + event_loop = QtCore.QEventLoop() + timer.timeout.connect(event_loop.quit) + while not stdin_ready(): + timer.start(50) + event_loop.exec_() + timer.stop() + except KeyboardInterrupt: + global got_kbdint, sigint_timer + + ignore_CTRL_C() + got_kbdint = True + mgr.clear_inputhook() + + # This generates a second SIGINT so the user doesn't have to + # press CTRL+C twice to get a clean prompt. + # + # Since we can't catch the resulting KeyboardInterrupt here + # (because this is a ctypes callback), we use a timer to + # generate the SIGINT after we leave this callback. + # + # Unfortunately this doesn't work on Windows (SIGINT kills + # Python and CTRL_C_EVENT doesn't work). + if(os.name == 'posix'): + pid = os.getpid() + if(not sigint_timer): + sigint_timer = threading.Timer(.01, os.kill, + args=[pid, signal.SIGINT] ) + sigint_timer.start() + else: + print("\nKeyboardInterrupt - Ctrl-C again for new prompt") + + + except: # NO exceptions are allowed to escape from a ctypes callback + ignore_CTRL_C() + from traceback import print_exc + print_exc() + print("Got exception from inputhook_qt5, unregistering.") + mgr.clear_inputhook() + finally: + allow_CTRL_C() + return 0 + + def preprompthook_qt5(ishell): + """'pre_prompt_hook' used to restore the Qt5 input hook + + (in case the latter was temporarily deactivated after a + CTRL+C) + """ + global got_kbdint, sigint_timer + + if(sigint_timer): + sigint_timer.cancel() + sigint_timer = None + + if got_kbdint: + mgr.set_inputhook(inputhook_qt5) + got_kbdint = False + + ip._inputhook_qt5 = inputhook_qt5 + ip.set_hook('pre_prompt_hook', preprompthook_qt5) + + return app, inputhook_qt5 diff --git a/python/helpers/pydev/pydev_ipython/matplotlibtools.py b/python/helpers/pydev/pydev_ipython/matplotlibtools.py index ea74b860fd52..9c186c1444a0 100644 --- a/python/helpers/pydev/pydev_ipython/matplotlibtools.py +++ b/python/helpers/pydev/pydev_ipython/matplotlibtools.py @@ -6,6 +6,7 @@ backends = {'tk': 'TkAgg', 'wx': 'WXAgg', 'qt': 'Qt4Agg', # qt3 not supported 'qt4': 'Qt4Agg', + 'qt5': 'Qt5Agg', 'osx': 'MacOSX'} # We also need a reverse backends2guis mapping that will properly choose which @@ -13,7 +14,8 @@ backends = {'tk': 'TkAgg', # most part it's just a reverse of the above dict, but we also need to add a # few others that map to the same GUI manually: backend2gui = dict(zip(backends.values(), backends.keys())) -backend2gui['Qt4Agg'] = 'qt' +backend2gui['Qt4Agg'] = 'qt4' +backend2gui['Qt5Agg'] = 'qt5' # In the reverse mapping, there are a few extra valid matplotlib backends that # map to the same GUI support backend2gui['GTK'] = backend2gui['GTKCairo'] = 'gtk' @@ -46,6 +48,7 @@ def find_gui_and_backend(): # In this case, we need to find what the appropriate gui selection call # should be for IPython, so we can activate inputhook accordingly gui = backend2gui.get(backend, None) + print("gui", gui) return gui, backend diff --git a/python/helpers/pydev/pydev_ipython/qt.py b/python/helpers/pydev/pydev_ipython/qt.py index 71c2fbcdaaa1..499ef940503e 100644 --- a/python/helpers/pydev/pydev_ipython/qt.py +++ b/python/helpers/pydev/pydev_ipython/qt.py @@ -9,14 +9,14 @@ Do not use this if you need PyQt with the old QString/QVariant API. import os from pydev_ipython.qt_loaders import (load_qt, QT_API_PYSIDE, - QT_API_PYQT) + QT_API_PYQT, QT_API_PYQT5) QT_API = os.environ.get('QT_API', None) -if QT_API not in [QT_API_PYSIDE, QT_API_PYQT, None]: +if QT_API not in [QT_API_PYSIDE, QT_API_PYQT, QT_API_PYQT5, None]: raise RuntimeError("Invalid Qt API %r, valid values are: %r, %r" % - (QT_API, QT_API_PYSIDE, QT_API_PYQT)) + (QT_API, QT_API_PYSIDE, QT_API_PYQT, QT_API_PYQT5)) if QT_API is None: - api_opts = [QT_API_PYSIDE, QT_API_PYQT] + api_opts = [QT_API_PYSIDE, QT_API_PYQT, QT_API_PYQT5] else: api_opts = [QT_API] diff --git a/python/helpers/pydev/pydev_ipython/qt_for_kernel.py b/python/helpers/pydev/pydev_ipython/qt_for_kernel.py index b1c986508a04..c50b0ab4681c 100644 --- a/python/helpers/pydev/pydev_ipython/qt_for_kernel.py +++ b/python/helpers/pydev/pydev_ipython/qt_for_kernel.py @@ -36,21 +36,26 @@ import sys from pydev_ipython.version import check_version from pydev_ipython.qt_loaders import (load_qt, QT_API_PYSIDE, - QT_API_PYQT, QT_API_PYQT_DEFAULT, - loaded_api) + QT_API_PYQT, QT_API_PYQT_DEFAULT, + loaded_api, QT_API_PYQT5) #Constraints placed on an imported matplotlib def matplotlib_options(mpl): if mpl is None: return mpqt = mpl.rcParams.get('backend.qt4', None) + if mpqt is None: + mpqt = mpl.rcParams.get('backend.qt5', None) + if mpqt is None: return None if mpqt.lower() == 'pyside': return [QT_API_PYSIDE] elif mpqt.lower() == 'pyqt4': return [QT_API_PYQT_DEFAULT] - raise ImportError("unhandled value for backend.qt4 from matplotlib: %r" % + elif mpqt.lower() == 'pyqt5': + return [QT_API_PYQT5] + raise ImportError("unhandled value for qt backend from matplotlib: %r" % mpqt) def get_options(): @@ -70,7 +75,7 @@ def get_options(): if os.environ.get('QT_API', None) is None: #no ETS variable. Ask mpl, then use either - return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE] + return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE, QT_API_PYQT5] #ETS variable present. Will fallback to external.qt return None diff --git a/python/helpers/pydev/pydev_ipython/qt_loaders.py b/python/helpers/pydev/pydev_ipython/qt_loaders.py index 7c81679dfc94..7178dae8a474 100644 --- a/python/helpers/pydev/pydev_ipython/qt_loaders.py +++ b/python/helpers/pydev/pydev_ipython/qt_loaders.py @@ -18,6 +18,7 @@ QT_API_PYQT = 'pyqt' QT_API_PYQTv1 = 'pyqtv1' QT_API_PYQT_DEFAULT = 'pyqtdefault' # don't set SIP explicitly QT_API_PYSIDE = 'pyside' +QT_API_PYQT5 = 'pyqt5' class ImportDenier(object): @@ -54,6 +55,7 @@ def commit_api(api): if api == QT_API_PYSIDE: ID.forbid('PyQt4') + ID.forbid('PyQt5') else: ID.forbid('PySide') @@ -75,6 +77,8 @@ def loaded_api(): return QT_API_PYQTv1 elif 'PySide.QtCore' in sys.modules: return QT_API_PYSIDE + elif 'PyQt5.QtCore' in sys.modules: + return QT_API_PYQT5 return None @@ -97,7 +101,9 @@ def has_binding(api): module_name = {QT_API_PYSIDE: 'PySide', QT_API_PYQT: 'PyQt4', QT_API_PYQTv1: 'PyQt4', - QT_API_PYQT_DEFAULT: 'PyQt4'} + QT_API_PYQT_DEFAULT: 'PyQt4', + QT_API_PYQT5: 'PyQt5', + } module_name = module_name[api] import imp @@ -142,7 +148,7 @@ def can_import(api): current = loaded_api() if api == QT_API_PYQT_DEFAULT: - return current in [QT_API_PYQT, QT_API_PYQTv1, None] + return current in [QT_API_PYQT, QT_API_PYQTv1, QT_API_PYQT5, None] else: return current in [api, None] @@ -157,7 +163,7 @@ def import_pyqt4(version=2): Which QString/QVariant API to use. Set to None to use the system default - ImportErrors rasied within this function are non-recoverable + ImportErrors raised within this function are non-recoverable """ # The new-style string API (version=2) automatically # converts QStrings to Unicode Python strings. Also, automatically unpacks @@ -183,6 +189,20 @@ def import_pyqt4(version=2): api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT return QtCore, QtGui, QtSvg, api +def import_pyqt5(): + """ + Import PyQt5 + + ImportErrors raised within this function are non-recoverable + """ + from PyQt5 import QtGui, QtCore, QtSvg + + # Alias PyQt-specific functions for PySide compatibility. + QtCore.Signal = QtCore.pyqtSignal + QtCore.Slot = QtCore.pyqtSlot + + return QtCore, QtGui, QtSvg, QT_API_PYQT5 + def import_pyside(): """ @@ -223,7 +243,8 @@ def load_qt(api_options): loaders = {QT_API_PYSIDE: import_pyside, QT_API_PYQT: import_pyqt4, QT_API_PYQTv1: partial(import_pyqt4, version=1), - QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None) + QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None), + QT_API_PYQT5: import_pyqt5, } for api in api_options: @@ -232,7 +253,7 @@ def load_qt(api_options): raise RuntimeError( "Invalid Qt API %r, valid values are: %r, %r, %r, %r" % (api, QT_API_PYSIDE, QT_API_PYQT, - QT_API_PYQTv1, QT_API_PYQT_DEFAULT)) + QT_API_PYQTv1, QT_API_PYQT_DEFAULT, QT_API_PYQT5)) if not can_import(api): continue @@ -250,9 +271,11 @@ def load_qt(api_options): Currently-imported Qt library: %r PyQt4 installed: %s + PyQt5 installed: %s PySide >= 1.0.3 installed: %s Tried to load: %r """ % (loaded_api(), has_binding(QT_API_PYQT), + has_binding(QT_API_PYQT5), has_binding(QT_API_PYSIDE), api_options))