Skip to content

Commit

Permalink
PR: Make QAction.setShortcut and setShortcuts accept many types (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ccordoba12 authored Nov 9, 2023
2 parents c046821 + c8d1144 commit a5f2548
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 24 deletions.
53 changes: 51 additions & 2 deletions qtpy/QtGui.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@

"""Provides QtGui classes and functions."""

from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QtModuleNotInstalledError
from ._utils import getattr_missing_optional_dep, possibly_static_exec
from functools import partialmethod

from packaging.version import parse

from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6
from . import QT_VERSION as _qt_version
from ._utils import (
getattr_missing_optional_dep,
possibly_static_exec,
set_shortcut,
set_shortcuts,
)

_missing_optional_names = {}

Expand Down Expand Up @@ -252,3 +262,42 @@ def movePositionPatched(
# Follow similar approach for `QDropEvent` and child classes
QDropEvent.pos = lambda self: self.position().toPoint()
QDropEvent.posF = lambda self: self.position()


# Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4
if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.4"):

class _QAction(QAction):
old_set_shortcut = QAction.setShortcut
old_set_shortcuts = QAction.setShortcuts

def setShortcut(self, shortcut):
return set_shortcut(
self,
shortcut,
old_set_shortcut=_QAction.old_set_shortcut,
)

def setShortcuts(self, shortcuts):
return set_shortcuts(
self,
shortcuts,
old_set_shortcuts=_QAction.old_set_shortcuts,
)

_action_set_shortcut = partialmethod(
set_shortcut,
old_set_shortcut=QAction.setShortcut,
)
_action_set_shortcuts = partialmethod(
set_shortcuts,
old_set_shortcuts=QAction.setShortcuts,
)
QAction.setShortcut = _action_set_shortcut
QAction.setShortcuts = _action_set_shortcuts
# Despite the two previous lines!
if (
QAction.setShortcut is not _action_set_shortcut
or QAction.setShortcuts is not _action_set_shortcuts
):
QAction = _QAction
48 changes: 42 additions & 6 deletions qtpy/QtWidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ def __getattr__(name):
elif PYQT6:
from PyQt6 import QtWidgets
from PyQt6.QtGui import (
QAction,
QActionGroup,
QFileSystemModel,
QShortcut,
QUndoCommand,
)
from PyQt6.QtWidgets import *

from qtpy.QtGui import QAction # See spyder-ide/qtpy#461

# Attempt to import QOpenGLWidget, but if that fails,
# don't raise an exception until the name is explicitly accessed.
# See https://github.com/spyder-ide/qtpy/pull/387/
Expand Down Expand Up @@ -110,9 +111,11 @@ def __getattr__(name):
elif PYSIDE2:
from PySide2.QtWidgets import *
elif PYSIDE6:
from PySide6.QtGui import QAction, QActionGroup, QShortcut, QUndoCommand
from PySide6.QtGui import QActionGroup, QShortcut, QUndoCommand
from PySide6.QtWidgets import *

from qtpy.QtGui import QAction # See spyder-ide/qtpy#461

# Attempt to import QOpenGLWidget, but if that fails,
# don't raise an exception until the name is explicitly accessed.
# See https://github.com/spyder-ide/qtpy/pull/387/
Expand Down Expand Up @@ -208,10 +211,43 @@ def __getattr__(name):
"directory",
)

# Make `addAction` compatible with Qt6 >= 6.3
if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.3"):
QMenu.addAction = partialmethod(add_action, old_add_action=QMenu.addAction)
QToolBar.addAction = partialmethod(
# Make `addAction` compatible with Qt6 >= 6.4
if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.4"):

class _QMenu(QMenu):
old_add_action = QMenu.addAction

def addAction(self, *args):
return add_action(
self,
*args,
old_add_action=_QMenu.old_add_action,
)

_menu_add_action = partialmethod(
add_action,
old_add_action=QMenu.addAction,
)
QMenu.addAction = _menu_add_action
# Despite the previous line!
if QMenu.addAction is not _menu_add_action:
QMenu = _QMenu

class _QToolBar(QToolBar):
old_add_action = QToolBar.addAction

def addAction(self, *args):
return add_action(
self,
*args,
old_add_action=_QToolBar.old_add_action,
)

_toolbar_add_action = partialmethod(
add_action,
old_add_action=QToolBar.addAction,
)
QToolBar.addAction = _toolbar_add_action
# Despite the previous line!
if QToolBar.addAction is not _toolbar_add_action:
QToolBar = _QToolBar
60 changes: 47 additions & 13 deletions qtpy/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,57 @@ def possibly_static_exec_(cls, *args, **kwargs):
return cls.exec_(*args, **kwargs)


def set_shortcut(self, shortcut, old_set_shortcut):
"""Ensure that the type of `shortcut` is compatible to `QAction.setShortcut`."""
from qtpy.QtCore import Qt
from qtpy.QtGui import QKeySequence

if isinstance(shortcut, (QKeySequence.StandardKey, Qt.Key, int)):
shortcut = QKeySequence(shortcut)
old_set_shortcut(self, shortcut)


def set_shortcuts(self, shortcuts, old_set_shortcuts):
"""Ensure that the type of `shortcuts` is compatible to `QAction.setShortcuts`."""
from qtpy.QtCore import Qt
from qtpy.QtGui import QKeySequence

if isinstance(
shortcuts,
(QKeySequence, QKeySequence.StandardKey, Qt.Key, int, str),
):
shortcuts = (shortcuts,)

shortcuts = tuple(
(
QKeySequence(shortcut)
if isinstance(shortcut, (QKeySequence.StandardKey, Qt.Key, int))
else shortcut
)
for shortcut in shortcuts
)
old_set_shortcuts(self, shortcuts)


def add_action(self, *args, old_add_action):
"""Re-order arguments of `addAction` to backport compatibility with Qt>=6.3."""
from qtpy.QtCore import QObject
from qtpy.QtCore import QObject, Qt
from qtpy.QtGui import QIcon, QKeySequence

action: QAction
icon: QIcon
text: str
shortcut: QKeySequence | QKeySequence.StandardKey | str | int
shortcut: QKeySequence | QKeySequence.StandardKey | Qt.Key | str | int
receiver: QObject
member: bytes

if all(
isinstance(arg, t)
for arg, t in zip(
args,
[
str,
(QKeySequence, QKeySequence.StandardKey, str, int),
(QKeySequence, QKeySequence.StandardKey, Qt.Key, str, int),
QObject,
bytes,
],
Expand All @@ -105,16 +138,15 @@ def add_action(self, *args, old_add_action):
text, shortcut, receiver, member = args
action = old_add_action(self, text, receiver, member, shortcut)
else:
return old_add_action(self, *args)
return action
if all(
action = old_add_action(self, *args)
elif all(
isinstance(arg, t)
for arg, t in zip(
args,
[
QIcon,
str,
(QKeySequence, QKeySequence.StandardKey, str, int),
(QKeySequence, QKeySequence.StandardKey, Qt.Key, str, int),
QObject,
bytes,
],
Expand All @@ -123,11 +155,11 @@ def add_action(self, *args, old_add_action):
if len(args) == 3:
icon, text, shortcut = args
action = old_add_action(self, icon, text)
action.setShortcut(QKeySequence(shortcut))
action.setShortcut(shortcut)
elif len(args) == 4:
icon, text, shortcut, receiver = args
action = old_add_action(self, icon, text, receiver)
action.setShortcut(QKeySequence(shortcut))
action.setShortcut(shortcut)
elif len(args) == 5:
icon, text, shortcut, receiver, member = args
action = old_add_action(
Expand All @@ -136,12 +168,14 @@ def add_action(self, *args, old_add_action):
text,
receiver,
member,
QKeySequence(shortcut),
shortcut,
)
else:
return old_add_action(self, *args)
return action
return old_add_action(self, *args)
action = old_add_action(self, *args)
else:
action = old_add_action(self, *args)

return action


def static_method_kwargs_wrapper(func, from_kwarg_name, to_kwarg_name):
Expand Down
36 changes: 36 additions & 0 deletions qtpy/tests/test_qtgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import sys

import pytest
from packaging.version import parse

from qtpy import (
PYQT5,
PYQT_VERSION,
PYSIDE2,
PYSIDE6,
QT_VERSION,
QtCore,
QtGui,
QtWidgets,
Expand Down Expand Up @@ -177,6 +179,40 @@ def test_qtextcursor_moveposition():
assert cursor.selectedText() == "foo bar baz"


@pytest.mark.skipif(
sys.platform == "darwin" and sys.version_info[:2] == (3, 7),
reason="Stalls on macOS CI with Python 3.7",
)
def test_QAction_functions(qtbot):
"""Test `QtGui.QAction.setShortcut` compatibility with Qt6 types."""
action = QtGui.QAction("QtPy", None)
action.setShortcut(QtGui.QKeySequence.UnknownKey)
action.setShortcuts([QtGui.QKeySequence.UnknownKey])
action.setShortcuts(QtGui.QKeySequence.UnknownKey)
action.setShortcut(QtCore.Qt.Key_F1)
action.setShortcuts([QtCore.Qt.Key_F1])
# The following line fails even for Qt6 == 6.6.
# Don't test the function with a single `QtCore.Qt.Key` argument.
# See the following test.
# action.setShortcuts(QtCore.Qt.Key_F1)


@pytest.mark.skipif(
parse(QT_VERSION) < parse("6.5.0"),
reason="Qt6 >= 6.5 specific test",
)
@pytest.mark.skipif(
sys.platform == "darwin" and sys.version_info[:2] == (3, 7),
reason="Stalls on macOS CI with Python 3.7",
)
@pytest.mark.xfail(strict=True)
def test_QAction_functions_fail(qtbot):
"""Test `QtGui.QAction.setShortcuts` compatibility with `QtCore.Qt.Key` type."""
action = QtGui.QAction("QtPy", None)
# The following line is wrong even for Qt6 == 6.6.
action.setShortcuts(QtCore.Qt.Key_F1)


def test_opengl_imports():
"""
Test for presence of QOpenGL* classes.
Expand Down
6 changes: 3 additions & 3 deletions qtpy/tests/test_qtwidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ def test_QMenu_functions(qtbot):
window = QtWidgets.QMainWindow()
menu = QtWidgets.QMenu(window)
menu.addAction("QtPy")
menu.addAction("QtPy with a shortcut", QtGui.QKeySequence.UnknownKey)
menu.addAction("QtPy with a Qt.Key shortcut", QtCore.Qt.Key_F1)
menu.addAction(
QtGui.QIcon(),
"QtPy with an icon and a shortcut",
"QtPy with an icon and a QKeySequence shortcut",
QtGui.QKeySequence.UnknownKey,
)
window.show()
Expand Down Expand Up @@ -148,7 +148,7 @@ def test_QMenu_functions(qtbot):
def test_QToolBar_functions(qtbot):
"""Test `QtWidgets.QToolBar.addAction` compatibility with Qt6 arguments' order."""
toolbar = QtWidgets.QToolBar()
toolbar.addAction("QtPy with a shortcut", QtGui.QKeySequence.UnknownKey)
toolbar.addAction("QtPy with a shortcut", QtCore.Qt.Key_F1)
toolbar.addAction(
QtGui.QIcon(),
"QtPy with an icon and a shortcut",
Expand Down

0 comments on commit a5f2548

Please sign in to comment.