Skip to content

Commit

Permalink
Merge pull request #1549 from wmvanvliet/qt6
Browse files Browse the repository at this point in the history
PyQt6 support
  • Loading branch information
AdamYoblick authored Jul 31, 2024
2 parents 7b3dfb8 + afaee41 commit dc58df1
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 17 deletions.
22 changes: 20 additions & 2 deletions src/debugpy/_vendored/pydevd/pydev_ipython/inputhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
GUI_QT = 'qt'
GUI_QT4 = 'qt4'
GUI_QT5 = 'qt5'
GUI_QT6 = 'qt6'
GUI_GTK = 'gtk'
GUI_TK = 'tk'
GUI_OSX = 'osx'
Expand Down Expand Up @@ -173,8 +174,10 @@ def disable_wx(self):
self.clear_inputhook()

def enable_qt(self, app=None):
from pydev_ipython.qt_for_kernel import QT_API, QT_API_PYQT5
if QT_API == QT_API_PYQT5:
from pydev_ipython.qt_for_kernel import QT_API, QT_API_PYQT5, QT_API_PYQT6
if QT_API == QT_API_PYQT6:
self.enable_qt6(app)
elif QT_API == QT_API_PYQT5:
self.enable_qt5(app)
else:
self.enable_qt4(app)
Expand Down Expand Up @@ -234,6 +237,21 @@ def disable_qt5(self):
self._apps[GUI_QT5]._in_event_loop = False
self.clear_inputhook()

def enable_qt6(self, app=None):
from pydev_ipython.inputhookqt6 import create_inputhook_qt6
app, inputhook_qt6 = create_inputhook_qt6(self, app)
self.set_inputhook(inputhook_qt6)

self._current_gui = GUI_QT6
app._in_event_loop = True
self._apps[GUI_QT6] = app
return app

def disable_qt6(self):
if GUI_QT6 in self._apps:
self._apps[GUI_QT6]._in_event_loop = False
self.clear_inputhook()

def enable_gtk(self, app=None):
"""Enable event loop integration with PyGTK.
Expand Down
199 changes: 199 additions & 0 deletions src/debugpy/_vendored/pydevd/pydev_ipython/inputhookqt6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# -*- coding: utf-8 -*-
"""
Qt6's inputhook support function
Author: Christian Boos, Marijn van Vliet
"""

#-----------------------------------------------------------------------------
# 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_qt6(mgr, app=None):
"""Create an input hook for running the Qt6 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 PyQt6'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 PyQt6 import QtWidgets
app = QtWidgets.QApplication([" "])

# Re-use previously created inputhook if any
ip = InteractiveShell.instance()
if hasattr(ip, '_inputhook_qt6'):
return app, ip._inputhook_qt6

# Otherwise create the inputhook_qt6/preprompthook_qt6 pair of
# hooks (they both share the got_kbdint flag)

def inputhook_qt6():
"""PyOS_InputHook python hook for Qt6.
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.ProcessEventsFlag.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_qt6, unregistering.")
mgr.clear_inputhook()
finally:
allow_CTRL_C()
return 0

def preprompthook_qt6(ishell):
"""'pre_prompt_hook' used to restore the Qt6 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_qt6)
got_kbdint = False

ip._inputhook_qt6 = inputhook_qt6
ip.set_hook('pre_prompt_hook', preprompthook_qt6)

return app, inputhook_qt6
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
'qt': 'QtAgg', # Auto-choose qt4/5
'qt4': 'Qt4Agg',
'qt5': 'Qt5Agg',
'qt6': 'Qt6Agg',
'osx': 'MacOSX'}

# We also need a reverse backends2guis mapping that will properly choose which
Expand Down
12 changes: 6 additions & 6 deletions src/debugpy/_vendored/pydevd/pydev_ipython/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

import os

from pydev_ipython.qt_loaders import (load_qt, QT_API_PYSIDE,
QT_API_PYQT, QT_API_PYQT5)
from pydev_ipython.qt_loaders import (load_qt, QT_API_PYSIDE, QT_API_PYSIDE2,
QT_API_PYQT, QT_API_PYQT5, QT_API_PYQT6)

QT_API = os.environ.get('QT_API', 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_PYQT5))
if QT_API not in [QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, QT_API_PYQT6, None]:
raise RuntimeError("Invalid Qt API %r, valid values are: %r, %r, %r, %r, %r" %
(QT_API, QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, QT_API_PYQT6))
if QT_API is None:
api_opts = [QT_API_PYSIDE, QT_API_PYQT, QT_API_PYQT5]
api_opts = [QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, QT_API_PYQT6]
else:
api_opts = [QT_API]

Expand Down
17 changes: 15 additions & 2 deletions src/debugpy/_vendored/pydevd/pydev_ipython/qt_for_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from pydev_ipython.version import check_version
from pydev_ipython.qt_loaders import (load_qt, QT_API_PYSIDE, QT_API_PYSIDE2,
QT_API_PYQT, QT_API_PYQT_DEFAULT,
loaded_api, QT_API_PYQT5)
loaded_api, QT_API_PYQT5, QT_API_PYQT6)


# Constraints placed on an imported matplotlib
Expand Down Expand Up @@ -71,10 +71,21 @@ def matplotlib_options(mpl):
raise ImportError("unhandled value for backend.qt5 from matplotlib: %r" %
mpqt)

elif backend == 'Qt6Agg':
mpqt = mpl.rcParams.get('backend.qt6', None)
if mpqt is None:
return None
if mpqt.lower() == 'pyqt6':
return [QT_API_PYQT6]
raise ImportError("unhandled value for backend.qt6 from matplotlib: %r" %
mpqt)

# Fallback without checking backend (previous code)
mpqt = mpl.rcParams.get('backend.qt4', None)
if mpqt is None:
mpqt = mpl.rcParams.get('backend.qt5', None)
if mpqt is None:
mpqt = mpl.rcParams.get('backend.qt6', None)

if mpqt is None:
return None
Expand All @@ -84,6 +95,8 @@ def matplotlib_options(mpl):
return [QT_API_PYQT_DEFAULT]
elif mpqt.lower() == 'pyqt5':
return [QT_API_PYQT5]
elif mpqt.lower() == 'pyqt6':
return [QT_API_PYQT6]
raise ImportError("unhandled value for qt backend from matplotlib: %r" %
mpqt)

Expand All @@ -105,7 +118,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, QT_API_PYSIDE2, QT_API_PYQT5]
return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT5, QT_API_PYQT6]

# ETS variable present. Will fallback to external.qt
return None
Expand Down
Loading

0 comments on commit dc58df1

Please sign in to comment.