qt_compat.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. """
  2. Qt binding and backend selector.
  3. The selection logic is as follows:
  4. - if any of PyQt5, PySide2, PyQt4 or PySide have already been imported
  5. (checked in that order), use it;
  6. - otherwise, if the QT_API environment variable (used by Enthought) is set, use
  7. it to determine which binding to use (but do not change the backend based on
  8. it; i.e. if the Qt5Agg backend is requested but QT_API is set to "pyqt4",
  9. then actually use Qt5 with PyQt5 or PySide2 (whichever can be imported);
  10. - otherwise, use whatever the rcParams indicate.
  11. Support for PyQt4 is deprecated.
  12. """
  13. from distutils.version import LooseVersion
  14. import os
  15. import sys
  16. import matplotlib as mpl
  17. QT_API_PYQT5 = "PyQt5"
  18. QT_API_PYSIDE2 = "PySide2"
  19. QT_API_PYQTv2 = "PyQt4v2"
  20. QT_API_PYSIDE = "PySide"
  21. QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2).
  22. QT_API_ENV = os.environ.get("QT_API")
  23. # Mapping of QT_API_ENV to requested binding. ETS does not support PyQt4v1.
  24. # (https://github.com/enthought/pyface/blob/master/pyface/qt/__init__.py)
  25. _ETS = {"pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2,
  26. "pyqt": QT_API_PYQTv2, "pyside": QT_API_PYSIDE,
  27. None: None}
  28. # First, check if anything is already imported.
  29. if "PyQt5.QtCore" in sys.modules:
  30. QT_API = QT_API_PYQT5
  31. elif "PySide2.QtCore" in sys.modules:
  32. QT_API = QT_API_PYSIDE2
  33. elif "PyQt4.QtCore" in sys.modules:
  34. QT_API = QT_API_PYQTv2
  35. elif "PySide.QtCore" in sys.modules:
  36. QT_API = QT_API_PYSIDE
  37. # Otherwise, check the QT_API environment variable (from Enthought). This can
  38. # only override the binding, not the backend (in other words, we check that the
  39. # requested backend actually matches). Use dict.__getitem__ to avoid
  40. # triggering backend resolution (which can result in a partially but
  41. # incompletely imported backend_qt5).
  42. elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt5Agg", "Qt5Cairo"]:
  43. if QT_API_ENV in ["pyqt5", "pyside2"]:
  44. QT_API = _ETS[QT_API_ENV]
  45. else:
  46. QT_API = None
  47. elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt4Agg", "Qt4Cairo"]:
  48. if QT_API_ENV in ["pyqt4", "pyside"]:
  49. QT_API = _ETS[QT_API_ENV]
  50. else:
  51. QT_API = None
  52. # A non-Qt backend was selected but we still got there (possible, e.g., when
  53. # fully manually embedding Matplotlib in a Qt app without using pyplot).
  54. else:
  55. try:
  56. QT_API = _ETS[QT_API_ENV]
  57. except KeyError as err:
  58. raise RuntimeError(
  59. "The environment variable QT_API has the unrecognized value {!r};"
  60. "valid values are 'pyqt5', 'pyside2', 'pyqt', and "
  61. "'pyside'") from err
  62. def _setup_pyqt5():
  63. global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
  64. _isdeleted, _getSaveFileName
  65. if QT_API == QT_API_PYQT5:
  66. from PyQt5 import QtCore, QtGui, QtWidgets
  67. import sip
  68. __version__ = QtCore.PYQT_VERSION_STR
  69. QtCore.Signal = QtCore.pyqtSignal
  70. QtCore.Slot = QtCore.pyqtSlot
  71. QtCore.Property = QtCore.pyqtProperty
  72. _isdeleted = sip.isdeleted
  73. elif QT_API == QT_API_PYSIDE2:
  74. from PySide2 import QtCore, QtGui, QtWidgets, __version__
  75. import shiboken2
  76. def _isdeleted(obj): return not shiboken2.isValid(obj)
  77. else:
  78. raise ValueError("Unexpected value for the 'backend.qt5' rcparam")
  79. _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName
  80. @mpl.cbook.deprecated("3.3", alternative="QtCore.qVersion()")
  81. def is_pyqt5():
  82. return True
  83. def _setup_pyqt4():
  84. global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
  85. _isdeleted, _getSaveFileName
  86. def _setup_pyqt4_internal(api):
  87. global QtCore, QtGui, QtWidgets, \
  88. __version__, is_pyqt5, _isdeleted, _getSaveFileName
  89. # List of incompatible APIs:
  90. # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
  91. _sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime",
  92. "QUrl", "QVariant"]
  93. try:
  94. import sip
  95. except ImportError:
  96. pass
  97. else:
  98. for _sip_api in _sip_apis:
  99. try:
  100. sip.setapi(_sip_api, api)
  101. except ValueError:
  102. pass
  103. from PyQt4 import QtCore, QtGui
  104. import sip # Always succeeds *after* importing PyQt4.
  105. __version__ = QtCore.PYQT_VERSION_STR
  106. # PyQt 4.6 introduced getSaveFileNameAndFilter:
  107. # https://riverbankcomputing.com/news/pyqt-46
  108. if __version__ < LooseVersion("4.6"):
  109. raise ImportError("PyQt<4.6 is not supported")
  110. QtCore.Signal = QtCore.pyqtSignal
  111. QtCore.Slot = QtCore.pyqtSlot
  112. QtCore.Property = QtCore.pyqtProperty
  113. _isdeleted = sip.isdeleted
  114. _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter
  115. if QT_API == QT_API_PYQTv2:
  116. _setup_pyqt4_internal(api=2)
  117. elif QT_API == QT_API_PYSIDE:
  118. from PySide import QtCore, QtGui, __version__, __version_info__
  119. import shiboken
  120. # PySide 1.0.3 fixed the following:
  121. # https://srinikom.github.io/pyside-bz-archive/809.html
  122. if __version_info__ < (1, 0, 3):
  123. raise ImportError("PySide<1.0.3 is not supported")
  124. def _isdeleted(obj): return not shiboken.isValid(obj)
  125. _getSaveFileName = QtGui.QFileDialog.getSaveFileName
  126. elif QT_API == QT_API_PYQT:
  127. _setup_pyqt4_internal(api=1)
  128. else:
  129. raise ValueError("Unexpected value for the 'backend.qt4' rcparam")
  130. QtWidgets = QtGui
  131. @mpl.cbook.deprecated("3.3", alternative="QtCore.qVersion()")
  132. def is_pyqt5():
  133. return False
  134. if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]:
  135. _setup_pyqt5()
  136. elif QT_API in [QT_API_PYQTv2, QT_API_PYSIDE, QT_API_PYQT]:
  137. _setup_pyqt4()
  138. elif QT_API is None: # See above re: dict.__getitem__.
  139. if dict.__getitem__(mpl.rcParams, "backend") == "Qt4Agg":
  140. _candidates = [(_setup_pyqt4, QT_API_PYQTv2),
  141. (_setup_pyqt4, QT_API_PYSIDE),
  142. (_setup_pyqt4, QT_API_PYQT),
  143. (_setup_pyqt5, QT_API_PYQT5),
  144. (_setup_pyqt5, QT_API_PYSIDE2)]
  145. else:
  146. _candidates = [(_setup_pyqt5, QT_API_PYQT5),
  147. (_setup_pyqt5, QT_API_PYSIDE2),
  148. (_setup_pyqt4, QT_API_PYQTv2),
  149. (_setup_pyqt4, QT_API_PYSIDE),
  150. (_setup_pyqt4, QT_API_PYQT)]
  151. for _setup, QT_API in _candidates:
  152. try:
  153. _setup()
  154. except ImportError:
  155. continue
  156. break
  157. else:
  158. raise ImportError("Failed to import any qt binding")
  159. else: # We should not get there.
  160. raise AssertionError("Unexpected QT_API: {}".format(QT_API))
  161. # These globals are only defined for backcompatibility purposes.
  162. ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4),
  163. pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5))
  164. QT_RC_MAJOR_VERSION = int(QtCore.qVersion().split(".")[0])
  165. if QT_RC_MAJOR_VERSION == 4:
  166. mpl.cbook.warn_deprecated("3.3", name="support for Qt4")
  167. def _devicePixelRatioF(obj):
  168. """
  169. Return obj.devicePixelRatioF() with graceful fallback for older Qt.
  170. This can be replaced by the direct call when we require Qt>=5.6.
  171. """
  172. try:
  173. # Not available on Qt<5.6
  174. return obj.devicePixelRatioF() or 1
  175. except AttributeError:
  176. pass
  177. try:
  178. # Not available on Qt4 or some older Qt5.
  179. # self.devicePixelRatio() returns 0 in rare cases
  180. return obj.devicePixelRatio() or 1
  181. except AttributeError:
  182. return 1
  183. def _setDevicePixelRatioF(obj, val):
  184. """
  185. Call obj.setDevicePixelRatioF(val) with graceful fallback for older Qt.
  186. This can be replaced by the direct call when we require Qt>=5.6.
  187. """
  188. if hasattr(obj, 'setDevicePixelRatioF'):
  189. # Not available on Qt<5.6
  190. obj.setDevicePixelRatioF(val)
  191. elif hasattr(obj, 'setDevicePixelRatio'):
  192. # Not available on Qt4 or some older Qt5.
  193. obj.setDevicePixelRatio(val)