1# -*- coding: utf-8 -*-
2"""
3This module exists to smooth out some of the differences between PySide and PyQt4:
4
5* Automatically import either PyQt4 or PySide depending on availability
6* Allow to import QtCore/QtGui pyqtgraph.Qt without specifying which Qt wrapper
7  you want to use.
8* Declare QtCore.Signal, .Slot in PyQt4
9* Declare loadUiType function for Pyside
10
11"""
12
13import os, sys, re, time, subprocess, warnings
14
15
16PYSIDE = 'PySide'
17PYSIDE2 = 'PySide2'
18PYSIDE6 = 'PySide6'
19PYQT4 = 'PyQt4'
20PYQT5 = 'PyQt5'
21PYQT6 = 'PyQt6'
22
23QT_LIB = os.getenv('PYQTGRAPH_QT_LIB')
24
25## Automatically determine which Qt package to use (unless specified by
26## environment variable).
27## This is done by first checking to see whether one of the libraries
28## is already imported. If not, then attempt to import in the order
29## specified in libOrder.
30if QT_LIB is None:
31    libOrder = [PYQT5, PYSIDE2, PYSIDE6, PYQT6]
32
33    for lib in libOrder:
34        if lib in sys.modules:
35            QT_LIB = lib
36            break
37
38if QT_LIB is None:
39    for lib in libOrder:
40        try:
41            __import__(lib)
42            QT_LIB = lib
43            break
44        except ImportError:
45            pass
46
47if QT_LIB is None:
48    raise Exception("PyQtGraph requires one of PyQt5, PyQt6, PySide2 or PySide6; none of these packages could be imported.")
49
50
51class FailedImport(object):
52    """Used to defer ImportErrors until we are sure the module is needed.
53    """
54    def __init__(self, err):
55        self.err = err
56
57    def __getattr__(self, attr):
58        raise self.err
59
60
61# Make a loadUiType function like PyQt has
62
63# Credit:
64# http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313
65
66class _StringIO(object):
67    """Alternative to built-in StringIO needed to circumvent unicode/ascii issues"""
68    def __init__(self):
69        self.data = []
70
71    def write(self, data):
72        self.data.append(data)
73
74    def getvalue(self):
75        return ''.join(map(str, self.data)).encode('utf8')
76
77
78def _loadUiType(uiFile):
79    """
80    PySide lacks a "loadUiType" command like PyQt4's, so we have to convert
81    the ui file to py code in-memory first and then execute it in a
82    special frame to retrieve the form_class.
83
84    from stackoverflow: http://stackoverflow.com/a/14195313/3781327
85
86    seems like this might also be a legitimate solution, but I'm not sure
87    how to make PyQt4 and pyside look the same...
88        http://stackoverflow.com/a/8717832
89    """
90
91    pyside2uic = None
92    if QT_LIB == PYSIDE2:
93        try:
94            import pyside2uic
95        except ImportError:
96            # later versions of pyside2 have dropped pyside2uic; use the uic binary instead.
97            pyside2uic = None
98
99        if pyside2uic is None:
100            pyside2version = tuple(map(int, PySide2.__version__.split(".")))
101            if (5, 14) <= pyside2version < (5, 14, 2, 2):
102                warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15')
103
104    # get class names from ui file
105    import xml.etree.ElementTree as xml
106    parsed = xml.parse(uiFile)
107    widget_class = parsed.find('widget').get('class')
108    form_class = parsed.find('class').text
109
110    # convert ui file to python code
111    if pyside2uic is None:
112        uic_executable = QT_LIB.lower() + '-uic'
113        uipy = subprocess.check_output([uic_executable, uiFile])
114    else:
115        o = _StringIO()
116        with open(uiFile, 'r') as f:
117            pyside2uic.compileUi(f, o, indent=0)
118        uipy = o.getvalue()
119
120    # execute python code
121    pyc = compile(uipy, '<string>', 'exec')
122    frame = {}
123    exec(pyc, frame)
124
125    # fetch the base_class and form class based on their type in the xml from designer
126    form_class = frame['Ui_%s'%form_class]
127    base_class = eval('QtGui.%s'%widget_class)
128
129    return form_class, base_class
130
131
132# For historical reasons, pyqtgraph maintains a Qt4-ish interface back when
133# there wasn't a QtWidgets module. This _was_ done by monkey-patching all of
134# QtWidgets into the QtGui module. This monkey-patching modifies QtGui at a
135# global level.
136# To avoid this, we now maintain a local "mirror" of QtCore, QtGui and QtWidgets.
137# Thus, when monkey-patching happens later on in this file, they will only affect
138# the local modules and not the global modules.
139def _copy_attrs(src, dst):
140    for o in dir(src):
141        if not hasattr(dst, o):
142            setattr(dst, o, getattr(src, o))
143
144from . import QtCore, QtGui, QtWidgets
145
146if QT_LIB == PYQT5:
147    # We're using PyQt5 which has a different structure so we're going to use a shim to
148    # recreate the Qt4 structure for Qt5
149    import PyQt5.QtCore, PyQt5.QtGui, PyQt5.QtWidgets
150    _copy_attrs(PyQt5.QtCore, QtCore)
151    _copy_attrs(PyQt5.QtGui, QtGui)
152    _copy_attrs(PyQt5.QtWidgets, QtWidgets)
153
154    try:
155        from PyQt5 import sip
156    except ImportError:
157        # some Linux distros package it this way (e.g. Ubuntu)
158        import sip
159    from PyQt5 import uic
160
161    try:
162        from PyQt5 import QtSvg
163    except ImportError as err:
164        QtSvg = FailedImport(err)
165    try:
166        from PyQt5 import QtTest
167    except ImportError as err:
168        QtTest = FailedImport(err)
169
170    VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
171
172elif QT_LIB == PYQT6:
173    import PyQt6.QtCore, PyQt6.QtGui, PyQt6.QtWidgets
174    _copy_attrs(PyQt6.QtCore, QtCore)
175    _copy_attrs(PyQt6.QtGui, QtGui)
176    _copy_attrs(PyQt6.QtWidgets, QtWidgets)
177
178    from PyQt6 import sip, uic
179
180    try:
181        from PyQt6 import QtSvg
182    except ImportError as err:
183        QtSvg = FailedImport(err)
184    try:
185        from PyQt6 import QtOpenGLWidgets
186    except ImportError as err:
187        QtOpenGLWidgets = FailedImport(err)
188    try:
189        from PyQt6 import QtTest
190    except ImportError as err:
191        QtTest = FailedImport(err)
192
193    VERSION_INFO = 'PyQt6 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
194
195elif QT_LIB == PYSIDE2:
196    import PySide2.QtCore, PySide2.QtGui, PySide2.QtWidgets
197    _copy_attrs(PySide2.QtCore, QtCore)
198    _copy_attrs(PySide2.QtGui, QtGui)
199    _copy_attrs(PySide2.QtWidgets, QtWidgets)
200
201    try:
202        from PySide2 import QtSvg
203    except ImportError as err:
204        QtSvg = FailedImport(err)
205    try:
206        from PySide2 import QtTest
207    except ImportError as err:
208        QtTest = FailedImport(err)
209
210    import shiboken2 as shiboken
211    import PySide2
212    VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__
213elif QT_LIB == PYSIDE6:
214    import PySide6.QtCore, PySide6.QtGui, PySide6.QtWidgets
215    _copy_attrs(PySide6.QtCore, QtCore)
216    _copy_attrs(PySide6.QtGui, QtGui)
217    _copy_attrs(PySide6.QtWidgets, QtWidgets)
218
219    try:
220        from PySide6 import QtSvg
221    except ImportError as err:
222        QtSvg = FailedImport(err)
223    try:
224        from PySide6 import QtOpenGLWidgets
225    except ImportError as err:
226        QtOpenGLWidgets = FailedImport(err)
227    try:
228        from PySide6 import QtTest
229    except ImportError as err:
230        QtTest = FailedImport(err)
231
232    import shiboken6 as shiboken
233    import PySide6
234    VERSION_INFO = 'PySide6 ' + PySide6.__version__ + ' Qt ' + QtCore.__version__
235
236else:
237    raise ValueError("Invalid Qt lib '%s'" % QT_LIB)
238
239
240# common to PyQt5, PyQt6, PySide2 and PySide6
241if QT_LIB in [PYQT5, PYQT6, PYSIDE2, PYSIDE6]:
242    # We're using Qt5 which has a different structure so we're going to use a shim to
243    # recreate the Qt4 structure
244
245    if QT_LIB in [PYQT5, PYSIDE2]:
246        __QGraphicsItem_scale = QtWidgets.QGraphicsItem.scale
247
248        def scale(self, *args):
249            warnings.warn(
250                "Deprecated Qt API, will be removed in 0.13.0.",
251                DeprecationWarning, stacklevel=2
252            )
253            if args:
254                sx, sy = args
255                tr = self.transform()
256                tr.scale(sx, sy)
257                self.setTransform(tr)
258            else:
259                return __QGraphicsItem_scale(self)
260        QtWidgets.QGraphicsItem.scale = scale
261
262        def rotate(self, angle):
263            warnings.warn(
264                "Deprecated Qt API, will be removed in 0.13.0.",
265                DeprecationWarning, stacklevel=2
266            )
267            tr = self.transform()
268            tr.rotate(angle)
269            self.setTransform(tr)
270        QtWidgets.QGraphicsItem.rotate = rotate
271
272        def translate(self, dx, dy):
273            warnings.warn(
274                "Deprecated Qt API, will be removed in 0.13.0.",
275                DeprecationWarning, stacklevel=2
276            )
277            tr = self.transform()
278            tr.translate(dx, dy)
279            self.setTransform(tr)
280        QtWidgets.QGraphicsItem.translate = translate
281
282        def setMargin(self, i):
283            warnings.warn(
284                "Deprecated Qt API, will be removed in 0.13.0.",
285                DeprecationWarning, stacklevel=2
286            )
287            self.setContentsMargins(i, i, i, i)
288        QtWidgets.QGridLayout.setMargin = setMargin
289
290        def setResizeMode(self, *args):
291            warnings.warn(
292                "Deprecated Qt API, will be removed in 0.13.0.",
293                DeprecationWarning, stacklevel=2
294            )
295            self.setSectionResizeMode(*args)
296        QtWidgets.QHeaderView.setResizeMode = setResizeMode
297
298    # Import all QtWidgets objects into QtGui
299    for o in dir(QtWidgets):
300        if o.startswith('Q'):
301            setattr(QtGui, o, getattr(QtWidgets,o) )
302
303    QtGui.QApplication.setGraphicsSystem = None
304
305
306if QT_LIB in [PYQT6, PYSIDE6]:
307    # We're using Qt6 which has a different structure so we're going to use a shim to
308    # recreate the Qt5 structure
309
310    if not isinstance(QtOpenGLWidgets, FailedImport):
311        QtWidgets.QOpenGLWidget = QtOpenGLWidgets.QOpenGLWidget
312
313
314# Common to PySide2 and PySide6
315if QT_LIB in [PYSIDE2, PYSIDE6]:
316    QtVersion = QtCore.__version__
317    loadUiType = _loadUiType
318    isQObjectAlive = shiboken.isValid
319
320    # PySide does not implement qWait
321    if not isinstance(QtTest, FailedImport):
322        if not hasattr(QtTest.QTest, 'qWait'):
323            @staticmethod
324            def qWait(msec):
325                start = time.time()
326                QtGui.QApplication.processEvents()
327                while time.time() < start + msec * 0.001:
328                    QtGui.QApplication.processEvents()
329            QtTest.QTest.qWait = qWait
330
331
332# Common to PyQt5 and PyQt6
333if QT_LIB in [PYQT5, PYQT6]:
334    QtVersion = QtCore.QT_VERSION_STR
335
336    # PyQt, starting in v5.5, calls qAbort when an exception is raised inside
337    # a slot. To maintain backward compatibility (and sanity for interactive
338    # users), we install a global exception hook to override this behavior.
339    if sys.excepthook == sys.__excepthook__:
340        sys_excepthook = sys.excepthook
341        def pyqt_qabort_override(*args, **kwds):
342            return sys_excepthook(*args, **kwds)
343        sys.excepthook = pyqt_qabort_override
344
345    def isQObjectAlive(obj):
346        return not sip.isdeleted(obj)
347
348    loadUiType = uic.loadUiType
349
350    QtCore.Signal = QtCore.pyqtSignal
351
352# USE_XXX variables are deprecated
353USE_PYSIDE = QT_LIB == PYSIDE
354USE_PYQT4 = QT_LIB == PYQT4
355USE_PYQT5 = QT_LIB == PYQT5
356
357## Make sure we have Qt >= 5.12
358versionReq = [5, 12]
359m = re.match(r'(\d+)\.(\d+).*', QtVersion)
360if m is not None and list(map(int, m.groups())) < versionReq:
361    print(list(map(int, m.groups())))
362    raise Exception('pyqtgraph requires Qt version >= %d.%d  (your version is %s)' % (versionReq[0], versionReq[1], QtVersion))
363
364App = QtWidgets.QApplication
365# subclassing QApplication causes segfaults on PySide{2, 6} / Python 3.8.7+
366
367QAPP = None
368def mkQApp(name=None):
369    """
370    Creates new QApplication or returns current instance if existing.
371
372    ============== ========================================================
373    **Arguments:**
374    name           (str) Application name, passed to Qt
375    ============== ========================================================
376    """
377    global QAPP
378
379    def onPaletteChange(palette):
380        color = palette.base().color().name()
381        app = QtWidgets.QApplication.instance()
382        app.setProperty('darkMode', color.lower() != "#ffffff")
383
384    QAPP = QtGui.QApplication.instance()
385    if QAPP is None:
386        # hidpi handling
387        qtVersionCompare = tuple(map(int, QtVersion.split(".")))
388        if qtVersionCompare > (6, 0):
389            # Qt6 seems to support hidpi without needing to do anything so continue
390            pass
391        elif qtVersionCompare > (5, 14):
392            os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
393            QtGui.QApplication.setHighDpiScaleFactorRoundingPolicy(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
394        else:  # qt 5.12 and 5.13
395            QtGui.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
396            QtGui.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
397        QAPP = QtGui.QApplication(sys.argv or ["pyqtgraph"])
398        QAPP.paletteChanged.connect(onPaletteChange)
399        QAPP.paletteChanged.emit(QAPP.palette())
400
401    if name is not None:
402        QAPP.setApplicationName(name)
403    return QAPP
404
405
406# exec() is used within _loadUiType, so we define as exec_() here and rename in pg namespace
407def exec_():
408    app = mkQApp()
409    return app.exec() if hasattr(app, 'exec') else app.exec_()
410