add support for PyQt6 and PySide6

This commit is contained in:
Talley Lambert
2021-04-24 11:36:31 -04:00
parent 0f1be2fe89
commit cec8a7f7d0
11 changed files with 331 additions and 15 deletions

View File

@@ -4,11 +4,6 @@ repos:
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/myint/autoflake
rev: v1.4
hooks:
- id: autoflake
args: ["--in-place", "--remove-all-unused-imports"]
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.8.0 rev: 5.8.0
hooks: hooks:

View File

@@ -16,7 +16,7 @@ behave like a standard QSlider... just with multiple handles.
- Uses platform-specific Qt styles - Uses platform-specific Qt styles
- Supports style sheets - Supports style sheets
- Supports mouse wheel and keypress (soon) events - Supports mouse wheel and keypress (soon) events
- Supports both PyQt5 and PySide2 (via qtpy) - Supports PyQt5, PyQt6, PySide2 and PySide6
## Installation ## Installation

14
examples/basic.py Normal file
View File

@@ -0,0 +1,14 @@
from pyqrangeslider import QRangeSlider
from pyqrangeslider.qtcompat import API_NAME
from pyqrangeslider.qtcompat.QtWidgets import QApplication
print(API_NAME)
app = QApplication([])
slider = QRangeSlider()
slider.setMinimum(0)
slider.setMaximum(100)
slider.setValue((20, 80))
slider.show()
app.exec_()

View File

@@ -1,8 +1,8 @@
from typing import List, Sequence, Tuple from typing import List, Sequence, Tuple
from qtpy import QtGui from .qtcompat import QtGui
from qtpy.QtCore import QEvent, QPoint, QRect, QRectF, Qt, Signal from .qtcompat.QtCore import QEvent, QPoint, QPointF, QRect, QRectF, Qt, Signal
from qtpy.QtWidgets import ( from .qtcompat.QtWidgets import (
QApplication, QApplication,
QSlider, QSlider,
QStyle, QStyle,
@@ -290,9 +290,11 @@ class QRangeSlider(QSlider):
hdl_idx = 0 hdl_idx = 0
dist = float("inf") dist = float("inf")
if isinstance(pos, QPointF):
pos = QPoint(pos.x(), pos.y())
# TODO: this should be reversed, to prefer higher value handles # TODO: this should be reversed, to prefer higher value handles
for i, hdl in enumerate(self._handleRects(opt)): for i, hdl in enumerate(self._handleRects(opt)):
if pos in hdl: if hdl.contains(pos):
return ("handle", i) # TODO: use enum for 'handle' return ("handle", i) # TODO: use enum for 'handle'
hdl_center = self._pick(hdl.center()) hdl_center = self._pick(hdl.center())
abs_dist = abs(event_position - hdl_center) abs_dist = abs(event_position - hdl_center)
@@ -373,7 +375,7 @@ class QRangeSlider(QSlider):
e.accept() e.accept()
def _scrollByDelta( def _scrollByDelta(
self, orientation: Qt.Orientation, modifiers: Qt.KeyboardModifiers, delta: int self, orientation, modifiers: Qt.KeyboardModifiers, delta: int
) -> bool: ) -> bool:
steps_to_scroll = 0 steps_to_scroll = 0
pg_step = self.pageStep() pg_step = self.pageStep()

View File

@@ -1,7 +1,7 @@
import pytest import pytest
from qtpy.QtCore import Qt
from pyqrangeslider import QRangeSlider from pyqrangeslider import QRangeSlider
from pyqrangeslider.qtcompat.QtCore import Qt
@pytest.mark.parametrize("orientation", ["Horizontal", "Vertical"]) @pytest.mark.parametrize("orientation", ["Horizontal", "Vertical"])

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2014-2015 Colin Duquesnoy
# Copyright © 2009- The Spyder Development Team
#
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
Modified from qtpy.QtCore.
Provides QtCore classes and functions.
"""
# flake8: noqa
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, PythonQtError
if PYQT5:
from PyQt5.QtCore import QT_VERSION_STR as __version__
from PyQt5.QtCore import *
from PyQt5.QtCore import pyqtBoundSignal as SignalInstance
from PyQt5.QtCore import pyqtProperty as Property
from PyQt5.QtCore import pyqtSignal as Signal
from PyQt5.QtCore import pyqtSlot as Slot
# Those are imported from `import *`
del pyqtSignal, pyqtBoundSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR
elif PYQT6:
from PyQt6.QtCore import QT_VERSION_STR as __version__
from PyQt6.QtCore import *
from PyQt6.QtCore import pyqtBoundSignal as SignalInstance
from PyQt6.QtCore import pyqtProperty as Property
from PyQt6.QtCore import pyqtSignal as Signal
from PyQt6.QtCore import pyqtSlot as Slot
# backwards compat with PyQt5
# namespace moves:
for cls in (QEvent, Qt):
for attr in dir(cls):
if not attr[0].isupper():
continue
ns = getattr(cls, attr)
for name, val in vars(ns).items():
if not name.startswith("_"):
setattr(cls, name, val)
# Those are imported from `import *`
del pyqtSignal, pyqtBoundSignal, pyqtSlot, pyqtProperty, QT_VERSION_STR
elif PYSIDE2:
import PySide2.QtCore
__version__ = PySide2.QtCore.__version__
elif PYSIDE6:
import PySide6.QtCore
__version__ = PySide6.QtCore.__version__
else:
raise PythonQtError("No Qt bindings could be found")

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2014-2015 Colin Duquesnoy
# Copyright © 2009- The Spyder Development Team
#
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
Modified from qtpy.QtGui
Provides QtGui classes and functions.
"""
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, PythonQtError
if PYQT5:
from PyQt5.QtGui import *
elif PYSIDE2:
from PySide2.QtGui import *
elif PYQT6:
from PyQt6.QtGui import *
# backwards compat with PyQt5
# namespace moves:
for cls in (QPalette,):
for attr in dir(cls):
if not attr[0].isupper():
continue
ns = getattr(cls, attr)
for name, val in vars(ns).items():
if not name.startswith("_"):
setattr(cls, name, val)
def pos(self, *a):
_pos = self.position(*a)
return _pos.toPoint()
QMouseEvent.pos = pos
elif PYSIDE6:
pass
else:
raise PythonQtError("No Qt bindings could be found")

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2014-2015 Colin Duquesnoy
# Copyright © 2009- The Spyder Developmet Team
#
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
Modified from qtpy.QtWidgets
Provides widget classes and functions.
"""
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, PythonQtError
if PYQT5:
from PyQt5.QtWidgets import *
elif PYSIDE2:
from PySide2.QtWidgets import *
elif PYQT6:
from PyQt6.QtWidgets import *
# backwards compat with PyQt5
# namespace moves:
for cls in (QStyle, QSlider):
for attr in dir(cls):
if not attr[0].isupper():
continue
ns = getattr(cls, attr)
for name, val in vars(ns).items():
if not name.startswith("_"):
setattr(cls, name, val)
def exec_(self):
self.exec()
QApplication.exec_ = exec_
elif PYSIDE6:
pass
else:
raise PythonQtError("No Qt bindings could be found")

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2009- The Spyder Development Team
# Copyright © 2014-2015 Colin Duquesnoy
#
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
This file is borrowed from qtpy and modified to support PySide6/PyQt6 (drops PyQt4)
"""
import os
import platform
import sys
import warnings
from distutils.version import LooseVersion
class PythonQtError(RuntimeError):
"""Error raise if no bindings could be selected."""
class PythonQtWarning(Warning):
"""Warning if some features are not implemented in a binding."""
# Qt API environment variable name
QT_API = "QT_API"
# Names of the expected PyQt5 api
PYQT5_API = ["pyqt5"]
# Names of the expected PyQt5 api
PYQT6_API = ["pyqt6"]
# Names of the expected PySide2 api
PYSIDE2_API = ["pyside2"]
# Names of the expected PySide2 api
PYSIDE6_API = ["pyside6"]
# Detecting if a binding was specified by the user
binding_specified = QT_API in os.environ
# Setting a default value for QT_API
os.environ.setdefault(QT_API, "pyqt5")
API = os.environ[QT_API].lower()
initial_api = API
assert API in (PYQT5_API + PYQT6_API + PYSIDE2_API + PYSIDE6_API)
PYQT5 = True
PYSIDE2 = PYQT6 = PYSIDE6 = False
# When `FORCE_QT_API` is set, we disregard
# any previously imported python bindings.
if os.environ.get("FORCE_QT_API") is not None:
if "PyQt5" in sys.modules:
API = initial_api if initial_api in PYQT5_API else "pyqt5"
elif "PySide2" in sys.modules:
API = initial_api if initial_api in PYSIDE2_API else "pyside2"
elif "PyQt6" in sys.modules:
API = initial_api if initial_api in PYQT6_API else "pyqt6"
elif "PySide6" in sys.modules:
API = initial_api if initial_api in PYSIDE6_API else "pyside6"
if API in PYQT5_API:
try:
from PyQt5.QtCore import PYQT_VERSION_STR as PYQT_VERSION # noqa
from PyQt5.QtCore import QT_VERSION_STR as QT_VERSION # noqa
PYSIDE_VERSION = None # noqa
if sys.platform == "darwin":
macos_version = LooseVersion(platform.mac_ver()[0])
if macos_version < LooseVersion("10.10"):
if LooseVersion(QT_VERSION) >= LooseVersion("5.9"):
raise PythonQtError(
"Qt 5.9 or higher only works in "
"macOS 10.10 or higher. Your "
"program will fail in this "
"system."
)
elif macos_version < LooseVersion("10.11"):
if LooseVersion(QT_VERSION) >= LooseVersion("5.11"):
raise PythonQtError(
"Qt 5.11 or higher only works in "
"macOS 10.11 or higher. Your "
"program will fail in this "
"system."
)
del macos_version
except ImportError:
API = os.environ["QT_API"] = "pyside2"
if API in PYSIDE2_API:
try:
from PySide2 import __version__ as PYSIDE_VERSION # noqa
from PySide2.QtCore import __version__ as QT_VERSION # noqa
PYQT_VERSION = None # noqa
PYQT5 = False
PYSIDE2 = True
if sys.platform == "darwin":
macos_version = LooseVersion(platform.mac_ver()[0])
if macos_version < LooseVersion("10.11"):
if LooseVersion(QT_VERSION) >= LooseVersion("5.11"):
raise PythonQtError(
"Qt 5.11 or higher only works in "
"macOS 10.11 or higher. Your "
"program will fail in this "
"system."
)
del macos_version
except ImportError:
API = os.environ["QT_API"] = "pyqt6"
if API in PYQT6_API:
try:
from PyQt6.QtCore import PYQT_VERSION_STR as PYQT_VERSION # noqa
from PyQt6.QtCore import QT_VERSION_STR as QT_VERSION # noqa
PYSIDE_VERSION = None # noqa
PYQT5 = False
PYQT6 = True
except ImportError:
API = os.environ["QT_API"] = "pyside6"
if API in PYSIDE6_API:
try:
from PySide6 import __version__ as PYSIDE_VERSION # noqa
from PySide6.QtCore import __version__ as QT_VERSION # noqa
PYQT_VERSION = None # noqa
PYQT5 = False
PYSIDE6 = True
except ImportError:
raise PythonQtError("No Qt bindings could be found")
# If a correct API name is passed to QT_API and it could not be found,
# switches to another and informs through the warning
if API != initial_api and binding_specified:
warnings.warn(
'Selected binding "{}" could not be found, '
'using "{}"'.format(initial_api, API),
RuntimeWarning,
)
API_NAME = {
"pyqt5": "PyQt5",
"pyqt6": "PyQt6",
"pyside2": "PySide2",
"pyside6": "PySide6",
}[API]

View File

@@ -35,8 +35,6 @@ zip_safe = False
packages = find: packages = find:
python_requires = >=3.6 python_requires = >=3.6
setup_requires = setuptools_scm setup_requires = setuptools_scm
install_requires =
qtpy
[options.extras_require] [options.extras_require]
pyside2 = pyside2 pyside2 = pyside2
@@ -60,7 +58,7 @@ dev =
exclude = _version.py,.eggs,examples exclude = _version.py,.eggs,examples
max-line-length = 79 max-line-length = 79
docstring-convention = numpy docstring-convention = numpy
ignore = E203,W503,E501,C901 ignore = E203,W503,E501,C901,F403,F405
[isort] [isort]
profile=black profile=black

View File

@@ -1,6 +1,7 @@
# For more information about tox, see https://tox.readthedocs.io/en/latest/ # For more information about tox, see https://tox.readthedocs.io/en/latest/
[tox] [tox]
envlist = py{37,38,39}-{linux,macos,windows}-{pyqt,pyside} envlist = py{37,38,39}-{linux,macos,windows}-{pyqt,pyside}
toxworkdir=/tmp/.tox
[gh-actions] [gh-actions]
python = python =