Skip to content

PyQt6 support #1549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading