Skip to content
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

PR: Make QAction.setShortcut and setShortcuts accept many types #461

Merged
merged 16 commits into from
Nov 9, 2023
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
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
Loading