1# -*- coding: utf-8 -*-
2"""
3GraphicsView.py -   Extension of QGraphicsView
4Copyright 2010  Luke Campagnola
5Distributed under MIT/X11 license. See license.txt for more information.
6"""
7
8from ..Qt import QtCore, QtGui, QtWidgets, QT_LIB
9from ..Point import Point
10from ..GraphicsScene import GraphicsScene
11from .. import functions as fn
12from .. import getConfigOption
13
14__all__ = ['GraphicsView']
15
16
17class GraphicsView(QtGui.QGraphicsView):
18    """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the
19    viewed coordinate range. Also automatically creates a GraphicsScene and a central QGraphicsWidget
20    that is automatically scaled to the full view geometry.
21
22    This widget is the basis for :class:`PlotWidget <pyqtgraph.PlotWidget>`,
23    :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and the view widget in
24    :class:`ImageView <pyqtgraph.ImageView>`.
25
26    By default, the view coordinate system matches the widget's pixel coordinates and
27    automatically updates when the view is resized. This can be overridden by setting
28    autoPixelRange=False. The exact visible range can be set with setRange().
29
30    The view can be panned using the middle mouse button and scaled using the right mouse button if
31    enabled via enableMouse()  (but ordinarily, we use ViewBox for this functionality)."""
32
33    sigDeviceRangeChanged = QtCore.Signal(object, object)
34    sigDeviceTransformChanged = QtCore.Signal(object)
35    sigMouseReleased = QtCore.Signal(object)
36    sigSceneMouseMoved = QtCore.Signal(object)
37    #sigRegionChanged = QtCore.Signal(object)
38    sigScaleChanged = QtCore.Signal(object)
39    lastFileDir = None
40
41    def __init__(self, parent=None, useOpenGL=None, background='default'):
42        """
43        ==============  ============================================================
44        **Arguments:**
45        parent          Optional parent widget
46        useOpenGL       If True, the GraphicsView will use OpenGL to do all of its
47                        rendering. This can improve performance on some systems,
48                        but may also introduce bugs (the combination of
49                        QGraphicsView and QOpenGLWidget is still an 'experimental'
50                        feature of Qt)
51        background      Set the background color of the GraphicsView. Accepts any
52                        single argument accepted by
53                        :func:`mkColor <pyqtgraph.mkColor>`. By
54                        default, the background color is determined using the
55                        'backgroundColor' configuration option (see
56                        :func:`setConfigOptions <pyqtgraph.setConfigOptions>`).
57        ==============  ============================================================
58        """
59
60        self.closed = False
61
62        QtGui.QGraphicsView.__init__(self, parent)
63
64        # This connects a cleanup function to QApplication.aboutToQuit. It is
65        # called from here because we have no good way to react when the
66        # QApplication is created by the user.
67        # See pyqtgraph.__init__.py
68        from .. import _connectCleanup
69        _connectCleanup()
70
71        if useOpenGL is None:
72            useOpenGL = getConfigOption('useOpenGL')
73
74        self.useOpenGL(useOpenGL)
75        self.setCacheMode(self.CacheModeFlag.CacheBackground)
76
77        ## This might help, but it's probably dangerous in the general case..
78        #self.setOptimizationFlag(self.DontSavePainterState, True)
79
80        self.setBackgroundRole(QtGui.QPalette.ColorRole.NoRole)
81        self.setBackground(background)
82
83        self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
84        self.setFrameShape(QtGui.QFrame.Shape.NoFrame)
85        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
86        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
87        self.setTransformationAnchor(QtGui.QGraphicsView.ViewportAnchor.NoAnchor)
88        self.setResizeAnchor(QtGui.QGraphicsView.ViewportAnchor.AnchorViewCenter)
89        self.setViewportUpdateMode(QtGui.QGraphicsView.ViewportUpdateMode.MinimalViewportUpdate)
90
91
92        self.lockedViewports = []
93        self.lastMousePos = None
94        self.setMouseTracking(True)
95        self.aspectLocked = False
96        self.range = QtCore.QRectF(0, 0, 1, 1)
97        self.autoPixelRange = True
98        self.currentItem = None
99        self.clearMouse()
100        self.updateMatrix()
101        # GraphicsScene must have parent or expect crashes!
102        self.sceneObj = GraphicsScene(parent=self)
103        self.setScene(self.sceneObj)
104
105        ## Workaround for PySide crash
106        ## This ensures that the scene will outlive the view.
107        if QT_LIB == 'PySide':
108            self.sceneObj._view_ref_workaround = self
109
110        ## by default we set up a central widget with a grid layout.
111        ## this can be replaced if needed.
112        self.centralWidget = None
113        self.setCentralItem(QtGui.QGraphicsWidget())
114        self.centralLayout = QtGui.QGraphicsGridLayout()
115        self.centralWidget.setLayout(self.centralLayout)
116
117        self.mouseEnabled = False
118        self.scaleCenter = False  ## should scaling center around view center (True) or mouse click (False)
119        self.clickAccepted = False
120
121    def setAntialiasing(self, aa):
122        """Enable or disable default antialiasing.
123        Note that this will only affect items that do not specify their own antialiasing options."""
124        if aa:
125            self.setRenderHints(self.renderHints() | QtGui.QPainter.RenderHint.Antialiasing)
126        else:
127            self.setRenderHints(self.renderHints() & ~QtGui.QPainter.RenderHint.Antialiasing)
128
129    def setBackground(self, background):
130        """
131        Set the background color of the GraphicsView.
132        To use the defaults specified py pyqtgraph.setConfigOption, use background='default'.
133        To make the background transparent, use background=None.
134        """
135        self._background = background
136        if background == 'default':
137            background = getConfigOption('background')
138        brush = fn.mkBrush(background)
139        self.setBackgroundBrush(brush)
140
141    def paintEvent(self, ev):
142        self.scene().prepareForPaint()
143        return super().paintEvent(ev)
144
145    def render(self, *args, **kwds):
146        self.scene().prepareForPaint()
147        return super().render(*args, **kwds)
148
149
150    def close(self):
151        self.centralWidget = None
152        self.scene().clear()
153        self.currentItem = None
154        self.sceneObj = None
155        self.closed = True
156        self.setViewport(None)
157        super(GraphicsView, self).close()
158
159    def useOpenGL(self, b=True):
160        if b:
161            HAVE_OPENGL = hasattr(QtWidgets, 'QOpenGLWidget')
162            if not HAVE_OPENGL:
163                raise Exception("Requested to use OpenGL with QGraphicsView, but QOpenGLWidget is not available.")
164
165            v = QtWidgets.QOpenGLWidget()
166        else:
167            v = QtGui.QWidget()
168
169        self.setViewport(v)
170
171    def keyPressEvent(self, ev):
172        self.scene().keyPressEvent(ev)  ## bypass view, hand event directly to scene
173                                        ## (view likes to eat arrow key events)
174
175
176    def setCentralItem(self, item):
177        return self.setCentralWidget(item)
178
179    def setCentralWidget(self, item):
180        """Sets a QGraphicsWidget to automatically fill the entire view (the item will be automatically
181        resize whenever the GraphicsView is resized)."""
182        if self.centralWidget is not None:
183            self.scene().removeItem(self.centralWidget)
184        self.centralWidget = item
185        if item is not None:
186            self.sceneObj.addItem(item)
187            self.resizeEvent(None)
188
189    def addItem(self, *args):
190        return self.scene().addItem(*args)
191
192    def removeItem(self, *args):
193        return self.scene().removeItem(*args)
194
195    def enableMouse(self, b=True):
196        self.mouseEnabled = b
197        self.autoPixelRange = (not b)
198
199    def clearMouse(self):
200        self.mouseTrail = []
201        self.lastButtonReleased = None
202
203    def resizeEvent(self, ev):
204        if self.closed:
205            return
206        if self.autoPixelRange:
207            self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height())
208        GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False)  ## we do this because some subclasses like to redefine setRange in an incompatible way.
209        self.updateMatrix()
210
211    def updateMatrix(self, propagate=True):
212        self.setSceneRect(self.range)
213        if self.autoPixelRange:
214            self.resetTransform()
215        else:
216            if self.aspectLocked:
217                self.fitInView(self.range, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
218            else:
219                self.fitInView(self.range, QtCore.Qt.AspectRatioMode.IgnoreAspectRatio)
220
221        if propagate:
222            for v in self.lockedViewports:
223                v.setXRange(self.range, padding=0)
224
225        self.sigDeviceRangeChanged.emit(self, self.range)
226        self.sigDeviceTransformChanged.emit(self)
227
228    def viewRect(self):
229        """Return the boundaries of the view in scene coordinates"""
230        ## easier to just return self.range ?
231        r = QtCore.QRectF(self.rect())
232        return self.viewportTransform().inverted()[0].mapRect(r)
233
234    def visibleRange(self):
235        ## for backward compatibility
236        return self.viewRect()
237
238    def translate(self, dx, dy):
239        self.range.adjust(dx, dy, dx, dy)
240        self.updateMatrix()
241
242    def scale(self, sx, sy, center=None):
243        scale = [sx, sy]
244        if self.aspectLocked:
245            scale[0] = scale[1]
246
247        if self.scaleCenter:
248            center = None
249        if center is None:
250            center = self.range.center()
251
252        w = self.range.width()  / scale[0]
253        h = self.range.height() / scale[1]
254        self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top())  /scale[1], w, h)
255
256        self.updateMatrix()
257        self.sigScaleChanged.emit(self)
258
259    def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, disableAutoPixel=True):
260        if disableAutoPixel:
261            self.autoPixelRange=False
262        if newRect is None:
263            newRect = self.visibleRange()
264            padding = 0
265
266        padding = Point(padding)
267        newRect = QtCore.QRectF(newRect)
268        pw = newRect.width() * padding[0]
269        ph = newRect.height() * padding[1]
270        newRect = newRect.adjusted(-pw, -ph, pw, ph)
271        scaleChanged = False
272        if self.range.width() != newRect.width() or self.range.height() != newRect.height():
273            scaleChanged = True
274        self.range = newRect
275        #print "New Range:", self.range
276        if self.centralWidget is not None:
277            self.centralWidget.setGeometry(self.range)
278        self.updateMatrix(propagate)
279        if scaleChanged:
280            self.sigScaleChanged.emit(self)
281
282    def scaleToImage(self, image):
283        """Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase."""
284        pxSize = image.pixelSize()
285        image.setPxMode(True)
286        try:
287            self.sigScaleChanged.disconnect(image.setScaledMode)
288        except (TypeError, RuntimeError):
289            pass
290        tl = image.sceneBoundingRect().topLeft()
291        w = self.size().width() * pxSize[0]
292        h = self.size().height() * pxSize[1]
293        range = QtCore.QRectF(tl.x(), tl.y(), w, h)
294        GraphicsView.setRange(self, range, padding=0)
295        self.sigScaleChanged.connect(image.setScaledMode)
296
297
298
299    def lockXRange(self, v1):
300        if not v1 in self.lockedViewports:
301            self.lockedViewports.append(v1)
302
303    def setXRange(self, r, padding=0.05):
304        r1 = QtCore.QRectF(self.range)
305        r1.setLeft(r.left())
306        r1.setRight(r.right())
307        GraphicsView.setRange(self, r1, padding=[padding, 0], propagate=False)
308
309    def setYRange(self, r, padding=0.05):
310        r1 = QtCore.QRectF(self.range)
311        r1.setTop(r.top())
312        r1.setBottom(r.bottom())
313        GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False)
314
315    def wheelEvent(self, ev):
316        super().wheelEvent(ev)
317        if not self.mouseEnabled:
318            return
319        delta = ev.angleDelta().x()
320        if delta == 0:
321            delta = ev.angleDelta().y()
322
323        sc = 1.001 ** delta
324        #self.scale *= sc
325        #self.updateMatrix()
326        self.scale(sc, sc)
327
328    def setAspectLocked(self, s):
329        self.aspectLocked = s
330
331    def leaveEvent(self, ev):
332        self.scene().leaveEvent(ev)  ## inform scene when mouse leaves
333
334    def mousePressEvent(self, ev):
335        super().mousePressEvent(ev)
336
337
338        if not self.mouseEnabled:
339            return
340        lpos = ev.position() if hasattr(ev, 'position') else ev.localPos()
341        self.lastMousePos = lpos
342        self.mousePressPos = lpos
343        self.clickAccepted = ev.isAccepted()
344        if not self.clickAccepted:
345            self.scene().clearSelection()
346        return   ## Everything below disabled for now..
347
348    def mouseReleaseEvent(self, ev):
349        super().mouseReleaseEvent(ev)
350        if not self.mouseEnabled:
351            return
352        self.sigMouseReleased.emit(ev)
353        self.lastButtonReleased = ev.button()
354        return   ## Everything below disabled for now..
355
356    def mouseMoveEvent(self, ev):
357        lpos = ev.position() if hasattr(ev, 'position') else ev.localPos()
358        if self.lastMousePos is None:
359            self.lastMousePos = lpos
360        delta = Point(lpos - self.lastMousePos)
361        self.lastMousePos = lpos
362
363        super().mouseMoveEvent(ev)
364        if not self.mouseEnabled:
365            return
366        self.sigSceneMouseMoved.emit(self.mapToScene(lpos))
367
368        if self.clickAccepted:  ## Ignore event if an item in the scene has already claimed it.
369            return
370
371        if ev.buttons() == QtCore.Qt.MouseButton.RightButton:
372            delta = Point(fn.clip_scalar(delta[0], -50, 50), fn.clip_scalar(-delta[1], -50, 50))
373            scale = 1.01 ** delta
374            self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
375            self.sigDeviceRangeChanged.emit(self, self.range)
376
377        elif ev.buttons() in [QtCore.Qt.MouseButton.MiddleButton, QtCore.Qt.MouseButton.LeftButton]:  ## Allow panning by left or mid button.
378            px = self.pixelSize()
379            tr = -delta * px
380
381            self.translate(tr[0], tr[1])
382            self.sigDeviceRangeChanged.emit(self, self.range)
383
384    def pixelSize(self):
385        """Return vector with the length and width of one view pixel in scene coordinates"""
386        p0 = Point(0,0)
387        p1 = Point(1,1)
388        tr = self.transform().inverted()[0]
389        p01 = tr.map(p0)
390        p11 = tr.map(p1)
391        return Point(p11 - p01)
392
393    def dragEnterEvent(self, ev):
394        ev.ignore()  ## not sure why, but for some reason this class likes to consume drag events
395