167 lines
6.0 KiB
Python
167 lines
6.0 KiB
Python
"""
|
|
Qt binding and backend selector.
|
|
|
|
The selection logic is as follows:
|
|
- if any of PyQt5, PySide2, PyQt4 or PySide have already been imported
|
|
(checked in that order), use it;
|
|
- otherwise, if the QT_API environment variable (used by Enthought) is set, use
|
|
it to determine which binding to use (but do not change the backend based on
|
|
it; i.e. if the Qt5Agg backend is requested but QT_API is set to "pyqt4",
|
|
then actually use Qt5 with PyQt5 or PySide2 (whichever can be imported);
|
|
- otherwise, use whatever the rcParams indicate.
|
|
"""
|
|
|
|
from distutils.version import LooseVersion
|
|
import os
|
|
import sys
|
|
|
|
from matplotlib import rcParams
|
|
|
|
|
|
QT_API_PYQT5 = "PyQt5"
|
|
QT_API_PYSIDE2 = "PySide2"
|
|
QT_API_PYQTv2 = "PyQt4v2"
|
|
QT_API_PYSIDE = "PySide"
|
|
QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2).
|
|
QT_API_ENV = os.environ.get("QT_API")
|
|
# Mapping of QT_API_ENV to requested binding. ETS does not support PyQt4v1.
|
|
# (https://github.com/enthought/pyface/blob/master/pyface/qt/__init__.py)
|
|
_ETS = {"pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2,
|
|
"pyqt": QT_API_PYQTv2, "pyside": QT_API_PYSIDE,
|
|
None: None}
|
|
# First, check if anything is already imported.
|
|
if "PyQt5.QtCore" in sys.modules:
|
|
QT_API = QT_API_PYQT5
|
|
elif "PySide2.QtCore" in sys.modules:
|
|
QT_API = QT_API_PYSIDE2
|
|
elif "PyQt4.QtCore" in sys.modules:
|
|
QT_API = QT_API_PYQTv2
|
|
elif "PySide.QtCore" in sys.modules:
|
|
QT_API = QT_API_PYSIDE
|
|
# Otherwise, check the QT_API environment variable (from Enthought). This can
|
|
# only override the binding, not the backend (in other words, we check that the
|
|
# requested backend actually matches).
|
|
elif rcParams["backend"] in ["Qt5Agg", "Qt5Cairo"]:
|
|
if QT_API_ENV in ["pyqt5", "pyside2"]:
|
|
QT_API = _ETS[QT_API_ENV]
|
|
else:
|
|
QT_API = None
|
|
elif rcParams["backend"] in ["Qt4Agg", "Qt4Cairo"]:
|
|
if QT_API_ENV in ["pyqt4", "pyside"]:
|
|
QT_API = _ETS[QT_API_ENV]
|
|
else:
|
|
QT_API = None
|
|
# A non-Qt backend was selected but we still got there (possible, e.g., when
|
|
# fully manually embedding Matplotlib in a Qt app without using pyplot).
|
|
else:
|
|
try:
|
|
QT_API = _ETS[QT_API_ENV]
|
|
except KeyError:
|
|
raise RuntimeError(
|
|
"The environment variable QT_API has the unrecognized value {!r};"
|
|
"valid values are 'pyqt5', 'pyside2', 'pyqt', and 'pyside'")
|
|
|
|
|
|
def _setup_pyqt5():
|
|
global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName
|
|
|
|
if QT_API == QT_API_PYQT5:
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
__version__ = QtCore.PYQT_VERSION_STR
|
|
QtCore.Signal = QtCore.pyqtSignal
|
|
QtCore.Slot = QtCore.pyqtSlot
|
|
QtCore.Property = QtCore.pyqtProperty
|
|
elif QT_API == QT_API_PYSIDE2:
|
|
from PySide2 import QtCore, QtGui, QtWidgets, __version__
|
|
else:
|
|
raise ValueError("Unexpected value for the 'backend.qt5' rcparam")
|
|
_getSaveFileName = QtWidgets.QFileDialog.getSaveFileName
|
|
|
|
def is_pyqt5():
|
|
return True
|
|
|
|
|
|
def _setup_pyqt4():
|
|
global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName
|
|
|
|
def _setup_pyqt4_internal(api):
|
|
global QtCore, QtGui, QtWidgets, \
|
|
__version__, is_pyqt5, _getSaveFileName
|
|
# List of incompatible APIs:
|
|
# http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
|
|
_sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime",
|
|
"QUrl", "QVariant"]
|
|
try:
|
|
import sip
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
for _sip_api in _sip_apis:
|
|
try:
|
|
sip.setapi(_sip_api, api)
|
|
except ValueError:
|
|
pass
|
|
from PyQt4 import QtCore, QtGui
|
|
__version__ = QtCore.PYQT_VERSION_STR
|
|
# PyQt 4.6 introduced getSaveFileNameAndFilter:
|
|
# https://riverbankcomputing.com/news/pyqt-46
|
|
if __version__ < LooseVersion("4.6"):
|
|
raise ImportError("PyQt<4.6 is not supported")
|
|
QtCore.Signal = QtCore.pyqtSignal
|
|
QtCore.Slot = QtCore.pyqtSlot
|
|
QtCore.Property = QtCore.pyqtProperty
|
|
_getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter
|
|
|
|
if QT_API == QT_API_PYQTv2:
|
|
_setup_pyqt4_internal(api=2)
|
|
elif QT_API == QT_API_PYSIDE:
|
|
from PySide import QtCore, QtGui, __version__, __version_info__
|
|
# PySide 1.0.3 fixed the following:
|
|
# https://srinikom.github.io/pyside-bz-archive/809.html
|
|
if __version_info__ < (1, 0, 3):
|
|
raise ImportError("PySide<1.0.3 is not supported")
|
|
_getSaveFileName = QtGui.QFileDialog.getSaveFileName
|
|
elif QT_API == QT_API_PYQT:
|
|
_setup_pyqt4_internal(api=1)
|
|
else:
|
|
raise ValueError("Unexpected value for the 'backend.qt4' rcparam")
|
|
QtWidgets = QtGui
|
|
|
|
def is_pyqt5():
|
|
return False
|
|
|
|
|
|
if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]:
|
|
_setup_pyqt5()
|
|
elif QT_API in [QT_API_PYQTv2, QT_API_PYSIDE, QT_API_PYQT]:
|
|
_setup_pyqt4()
|
|
elif QT_API is None:
|
|
if rcParams["backend"] == "Qt4Agg":
|
|
_candidates = [(_setup_pyqt4, QT_API_PYQTv2),
|
|
(_setup_pyqt4, QT_API_PYSIDE),
|
|
(_setup_pyqt4, QT_API_PYQT),
|
|
(_setup_pyqt5, QT_API_PYQT5),
|
|
(_setup_pyqt5, QT_API_PYSIDE2)]
|
|
else:
|
|
_candidates = [(_setup_pyqt5, QT_API_PYQT5),
|
|
(_setup_pyqt5, QT_API_PYSIDE2),
|
|
(_setup_pyqt4, QT_API_PYQTv2),
|
|
(_setup_pyqt4, QT_API_PYSIDE),
|
|
(_setup_pyqt4, QT_API_PYQT)]
|
|
for _setup, QT_API in _candidates:
|
|
try:
|
|
_setup()
|
|
except ImportError:
|
|
continue
|
|
break
|
|
else:
|
|
raise ImportError("Failed to import any qt binding")
|
|
else: # We should not get there.
|
|
raise AssertionError("Unexpected QT_API: {}".format(QT_API))
|
|
|
|
|
|
# These globals are only defined for backcompatibility purposes.
|
|
ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4),
|
|
pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5))
|
|
QT_RC_MAJOR_VERSION = 5 if is_pyqt5() else 4
|