Beispiel #1
0
 def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, name=None):
     GraphicsWidget.__init__(self, parent)
     self.name = None
     self.linksBlocked = False
     self.addedItems = []
     #self.gView = view
     #self.showGrid = showGrid
     
     self.state = {
         
         ## separating targetRange and viewRange allows the view to be resized
         ## while keeping all previously viewed contents visible
         'targetRange': [[0,1], [0,1]],   ## child coord. range visible [[xmin, xmax], [ymin, ymax]]
         'viewRange': [[0,1], [0,1]],     ## actual range viewed
     
         'yInverted': invertY,
         'aspectLocked': False,    ## False if aspect is unlocked, otherwise float specifies the locked ratio.
         'autoRange': [True, True],  ## False if auto range is disabled, 
                                       ## otherwise float gives the fraction of data that is visible
         'linkedViews': [None, None],
         
         'mouseEnabled': [enableMouse, enableMouse],
         'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode,  
         'wheelScaleFactor': -1.0 / 8.0,
     }
     
     
     self.exportMethods = collections.OrderedDict([
         ('SVG', self.saveSvg),
         ('Image', self.saveImage),
         ('Print', self.savePrint),
     ])
     
     self.setFlag(self.ItemClipsChildrenToShape)
     self.setFlag(self.ItemIsFocusable, True)  ## so we can receive key presses
     
     ## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
     ## this is a workaround for a Qt + OpenGL but that causes improper clipping
     ## https://bugreports.qt.nokia.com/browse/QTBUG-23723
     self.childGroup = ChildGroup(self)
     self.childGroup.sigItemsChanged.connect(self.itemsChanged)
     
     #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
     # this also enables capture of keyPressEvents.
     
     ## Make scale box that is shown when dragging on the view
     self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
     self.rbScaleBox.setPen(fn.mkPen((255,0,0), width=1))
     self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
     self.rbScaleBox.hide()
     self.addItem(self.rbScaleBox)
     
     self.axHistory = [] # maintain a history of zoom locations
     self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo"
     
     self.setZValue(-100)
     self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
     
     self.setAspectLocked(lockAspect)
     
     self.border = border
     self.menu = ViewBoxMenu(self)
     
     self.register(name)
     if name is None:
         self.updateViewLists()
Beispiel #2
0
    def __init__(self,
                 parent=None,
                 border=None,
                 lockAspect=False,
                 enableMouse=True,
                 invertY=False,
                 name=None):
        """
        =============  =============================================================
        **Arguments**
        *parent*       (QGraphicsWidget) Optional parent widget
        *border*       (QPen) Do draw a border around the view, give any 
                       single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
        *lockAspect*   (False or float) The aspect ratio to lock the view 
                       coorinates to. (or False to allow the ratio to change)
        *enableMouse*  (bool) Whether mouse can be used to scale/pan the view 
        *invertY*      (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
        =============  =============================================================
        """

        GraphicsWidget.__init__(self, parent)
        self.name = None
        self.linksBlocked = False
        self.addedItems = []
        #self.gView = view
        #self.showGrid = showGrid

        self.state = {

            ## separating targetRange and viewRange allows the view to be resized
            ## while keeping all previously viewed contents visible
            'targetRange':
            [[0, 1],
             [0,
              1]],  ## child coord. range visible [[xmin, xmax], [ymin, ymax]]
            'viewRange': [[0, 1], [0, 1]],  ## actual range viewed
            'yInverted':
            invertY,
            'aspectLocked':
            False,  ## False if aspect is unlocked, otherwise float specifies the locked ratio.
            'autoRange': [True, True],  ## False if auto range is disabled, 
            ## otherwise float gives the fraction of data that is visible
            'linkedViews': [None, None],
            'mouseEnabled': [enableMouse, enableMouse],
            'mouseMode':
            ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else
            ViewBox.RectMode,
            'wheelScaleFactor':
            -1.0 / 8.0,
        }

        #self.exportMethods = collections.OrderedDict([
        #('SVG', self.saveSvg),
        #('Image', self.saveImage),
        #('Print', self.savePrint),
        #])

        self.setFlag(self.ItemClipsChildrenToShape)
        self.setFlag(self.ItemIsFocusable,
                     True)  ## so we can receive key presses

        ## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
        ## this is a workaround for a Qt + OpenGL but that causes improper clipping
        ## https://bugreports.qt.nokia.com/browse/QTBUG-23723
        self.childGroup = ChildGroup(self)
        self.childGroup.sigItemsChanged.connect(self.itemsChanged)

        #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
        # this also enables capture of keyPressEvents.

        ## Make scale box that is shown when dragging on the view
        self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
        self.rbScaleBox.setPen(fn.mkPen((255, 0, 0), width=1))
        self.rbScaleBox.setBrush(fn.mkBrush(255, 255, 0, 100))
        self.rbScaleBox.hide()
        self.addItem(self.rbScaleBox)

        self.axHistory = []  # maintain a history of zoom locations
        self.axHistoryPointer = -1  # pointer into the history. Allows forward/backward movement, not just "undo"

        self.setZValue(-100)
        self.setSizePolicy(
            QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
                              QtGui.QSizePolicy.Expanding))

        self.setAspectLocked(lockAspect)

        self.border = fn.mkPen(border)
        self.menu = ViewBoxMenu(self)

        self.register(name)
        if name is None:
            self.updateViewLists()
Beispiel #3
0
class ViewBox(GraphicsWidget):
    """
    Box that allows internal scaling/panning of children by mouse drag. 
    Not really compatible with GraphicsView having the same functionality.
    """
    
    sigYRangeChanged = QtCore.Signal(object, object)
    sigXRangeChanged = QtCore.Signal(object, object)
    sigRangeChangedManually = QtCore.Signal(object)
    sigRangeChanged = QtCore.Signal(object, object)
    #sigActionPositionChanged = QtCore.Signal(object)
    sigStateChanged = QtCore.Signal(object)
    
    ## mouse modes
    PanMode = 3
    RectMode = 1
    
    ## axes
    XAxis = 0
    YAxis = 1
    XYAxes = 2
    
    ## for linking views together
    NamedViews = weakref.WeakValueDictionary()   # name: ViewBox
    AllViews = weakref.WeakKeyDictionary()       # ViewBox: None
    
    
    def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, name=None):
        GraphicsWidget.__init__(self, parent)
        self.name = None
        self.linksBlocked = False
        self.addedItems = []
        #self.gView = view
        #self.showGrid = showGrid
        
        self.state = {
            
            ## separating targetRange and viewRange allows the view to be resized
            ## while keeping all previously viewed contents visible
            'targetRange': [[0,1], [0,1]],   ## child coord. range visible [[xmin, xmax], [ymin, ymax]]
            'viewRange': [[0,1], [0,1]],     ## actual range viewed
        
            'yInverted': invertY,
            'aspectLocked': False,    ## False if aspect is unlocked, otherwise float specifies the locked ratio.
            'autoRange': [True, True],  ## False if auto range is disabled, 
                                          ## otherwise float gives the fraction of data that is visible
            'linkedViews': [None, None],
            
            'mouseEnabled': [enableMouse, enableMouse],
            'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode,  
            'wheelScaleFactor': -1.0 / 8.0,
        }
        
        
        self.exportMethods = collections.OrderedDict([
            ('SVG', self.saveSvg),
            ('Image', self.saveImage),
            ('Print', self.savePrint),
        ])
        
        self.setFlag(self.ItemClipsChildrenToShape)
        self.setFlag(self.ItemIsFocusable, True)  ## so we can receive key presses
        
        ## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
        ## this is a workaround for a Qt + OpenGL but that causes improper clipping
        ## https://bugreports.qt.nokia.com/browse/QTBUG-23723
        self.childGroup = ChildGroup(self)
        self.childGroup.sigItemsChanged.connect(self.itemsChanged)
        
        #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
        # this also enables capture of keyPressEvents.
        
        ## Make scale box that is shown when dragging on the view
        self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
        self.rbScaleBox.setPen(fn.mkPen((255,0,0), width=1))
        self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
        self.rbScaleBox.hide()
        self.addItem(self.rbScaleBox)
        
        self.axHistory = [] # maintain a history of zoom locations
        self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo"
        
        self.setZValue(-100)
        self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
        
        self.setAspectLocked(lockAspect)
        
        self.border = border
        self.menu = ViewBoxMenu(self)
        
        self.register(name)
        if name is None:
            self.updateViewLists()
        
    def register(self, name):
        """
        Add this ViewBox to the registered list of views. 
        *name* will appear in the drop-down lists for axis linking in all other views.
        The same can be accomplished by initializing the ViewBox with the *name* attribute.
        """
        ViewBox.AllViews[self] = None
        if self.name is not None:
            del ViewBox.NamedViews[self.name]
        self.name = name
        if name is not None:
            ViewBox.NamedViews[name] = self
            ViewBox.updateAllViewLists()

    def unregister(self):
        del ViewBox.AllViews[self]
        if self.name is not None:
            del ViewBox.NamedViews[self.name]

    def close(self):
        self.unregister()

    def implements(self, interface):
        return interface == 'ViewBox'
        
        
    def getState(self, copy=True):
        state = self.state.copy()
        state['linkedViews'] = [(None if v is None else v.name) for v in state['linkedViews']]
        if copy:
            return deepcopy(self.state)
        else:
            return self.state
        
    def setState(self, state):
        state = state.copy()
        self.setXLink(state['linkedViews'][0])
        self.setYLink(state['linkedViews'][1])
        del state['linkedViews']
        
        self.state.update(state)
        self.updateMatrix()
        self.sigStateChanged.emit(self)


    def setMouseMode(self, mode):
        if mode not in [ViewBox.PanMode, ViewBox.RectMode]:
            raise Exception("Mode must be ViewBox.PanMode or ViewBox.RectMode")
        self.state['mouseMode'] = mode
        self.sigStateChanged.emit(self)

    #def toggleLeftAction(self, act):  ## for backward compatibility
        #if act.text() is 'pan':
            #self.setLeftButtonAction('pan')
        #elif act.text() is 'zoom':
            #self.setLeftButtonAction('rect')

    def setLeftButtonAction(self, mode='rect'):  ## for backward compatibility
        if mode.lower() == 'rect':
            self.setMouseMode(ViewBox.RectMode)
        elif mode.lower() == 'pan':
            self.setMouseMode(ViewBox.PanMode)
        else:
            raise Exception('graphicsItems:ViewBox:setLeftButtonAction: unknown mode = %s (Options are "pan" and "rect")' % mode)
            
    def innerSceneItem(self):
        return self.childGroup
    
    def setMouseEnabled(self, x=None, y=None):
        if x is not None:
            self.state['mouseEnabled'][0] = x
        if y is not None:
            self.state['mouseEnabled'][1] = y
        self.sigStateChanged.emit(self)
            
    def mouseEnabled(self):
        return self.state['mouseEnabled'][:]
    
    def addItem(self, item, ignoreBounds=False):
        if item.zValue() < self.zValue():
            item.setZValue(self.zValue()+1)
        item.setParentItem(self.childGroup)
        if not ignoreBounds:
            self.addedItems.append(item)
        self.updateAutoRange()
        #print "addItem:", item, item.boundingRect()
        
    def removeItem(self, item):
        try:
            self.addedItems.remove(item)
        except:
            pass
        self.scene().removeItem(item)
        self.updateAutoRange()

    def resizeEvent(self, ev):
        #self.setRange(self.range, padding=0)
        #self.updateAutoRange()
        self.updateMatrix()
        self.sigStateChanged.emit(self)
        #self.linkedXChanged()
        #self.linkedYChanged()
        
    def viewRange(self):
        return [x[:] for x in self.state['viewRange']]  ## return copy

    def viewRect(self):
        """Return a QRectF bounding the region visible within the ViewBox"""
        try:
            vr0 = self.state['viewRange'][0]
            vr1 = self.state['viewRange'][1]
            return QtCore.QRectF(vr0[0], vr1[0], vr0[1]-vr0[0], vr1[1] - vr1[0])
        except:
            print "make qrectf failed:", self.state['viewRange']
            raise
    
    #def viewportTransform(self):
        ##return self.itemTransform(self.childGroup)[0]
        #return self.childGroup.itemTransform(self)[0]
    
    def targetRange(self):
        return [x[:] for x in self.state['targetRange']]  ## return copy
    
    def targetRect(self):  
        """
        Return the region which has been requested to be visible. 
        (this is not necessarily the same as the region that is *actually* visible--
        resizing and aspect ratio constraints can cause targetRect() and viewRect() to differ)
        """
        try:
            tr0 = self.state['targetRange'][0]
            tr1 = self.state['targetRange'][1]
            return QtCore.QRectF(tr0[0], tr1[0], tr0[1]-tr0[0], tr1[1] - tr1[0])
        except:
            print "make qrectf failed:", self.state['targetRange']
            raise

    def setRange(self, rect=None, xRange=None, yRange=None, padding=0.02, update=True, disableAutoRange=True):
        """
        Set the visible range of the ViewBox.
        Must specify at least one of *range*, *xRange*, or *yRange*. 
        
        Arguments:
            *rect* (QRectF)    - The full range that should be visible in the view box.
            *xRange* (min,max) - The range that should be visible along the x-axis.
            *yRange* (min,max) - The range that should be visible along the y-axis.
            *padding* (float)  - Expand the view by a fraction of the requested range
                                 By default, this value is 0.02 (2%)
        
        """
        changes = {}
        
        if rect is not None:
            changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]}
        if xRange is not None:
            changes[0] = xRange
        if yRange is not None:
            changes[1] = yRange

        if len(changes) == 0:
            raise Exception("Must specify at least one of rect, xRange, or yRange.")
        
        changed = [False, False]
        for ax, range in changes.iteritems():
            mn = min(range)
            mx = max(range)
            if mn == mx:   ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale.
                dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0]
                if dy == 0:
                    dy = 1
                mn -= dy*0.5
                mx += dy*0.5
                padding = 0.0
            if any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])):
                raise Exception("Not setting range [%s, %s]" % (str(mn), str(mx)))
                
            p = (mx-mn) * padding
            mn -= p
            mx += p
            
            if self.state['targetRange'][ax] != [mn, mx]:
                self.state['targetRange'][ax] = [mn, mx]
                changed[ax] = True
            
        if any(changed) and disableAutoRange:
            if all(changed):
                ax = ViewBox.XYAxes
            elif changed[0]:
                ax = ViewBox.XAxis
            elif changed[1]:
                ax = ViewBox.YAxis
            self.enableAutoRange(ax, False)
                
                
        self.sigStateChanged.emit(self)
        
        if update:
            self.updateMatrix(changed)
            
        for ax, range in changes.iteritems():
            link = self.state['linkedViews'][ax]
            if link is not None:
                link.linkedViewChanged(self, ax)
        

            
    def setYRange(self, min, max, padding=0.02, update=True):
        self.setRange(yRange=[min, max], update=update, padding=padding)
        
    def setXRange(self, min, max, padding=0.02, update=True):
        self.setRange(xRange=[min, max], update=update, padding=padding)

    def autoRange(self, padding=0.02):
        """
        Set the range of the view box to make all children visible.
        """
        bounds = self.childrenBoundingRect()
        if bounds is not None:
            self.setRange(bounds, padding=padding)
            
            
    def scaleBy(self, s, center=None):
        """
        Scale by *s* around given center point (or center of view).
        *s* may be a Point or tuple (x, y)
        """
        scale = Point(s)
        if self.state['aspectLocked'] is not False:
            scale[0] = self.state['aspectLocked'] * scale[1]

        vr = self.targetRect()
        if center is None:
            center = Point(vr.center())
        else:
            center = Point(center)
        
        tl = center + (vr.topLeft()-center) * scale
        br = center + (vr.bottomRight()-center) * scale
       
        self.setRange(QtCore.QRectF(tl, br), padding=0)
        
    def translateBy(self, t):
        """
        Translate the view by *t*, which may be a Point or tuple (x, y).
        """
        t = Point(t)
        #if viewCoords:  ## scale from pixels
            #o = self.mapToView(Point(0,0))
            #t = self.mapToView(t) - o
        
        vr = self.targetRect()
        self.setRange(vr.translated(t), padding=0)
        
    def enableAutoRange(self, axis=None, enable=True):
        """
        Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both.
        When enabled, the axis will automatically rescale when items are added/removed or change their shape.
        The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should
        be visible (this only works with items implementing a dataRange method, such as PlotDataItem).
        """
        #print "autorange:", axis, enable
        #if not enable:
            #import traceback
            #traceback.print_stack()
        if enable is True:
            enable = 1.0
        
        if axis is None:
            axis = ViewBox.XYAxes
        
        if axis == ViewBox.XYAxes or axis == 'xy':
            self.state['autoRange'][0] = enable
            self.state['autoRange'][1] = enable
        elif axis == ViewBox.XAxis or axis == 'x':
            self.state['autoRange'][0] = enable
        elif axis == ViewBox.YAxis or axis == 'y':
            self.state['autoRange'][1] = enable
        else:
            raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.')
        
        if enable:
            self.updateAutoRange()
        self.sigStateChanged.emit(self)

    def disableAutoRange(self, axis=None):
        self.enableAutoRange(axis, enable=False)

    def autoRangeEnabled(self):
        return self.state['autoRange'][:]

    def updateAutoRange(self):
        tr = self.viewRect()
        if not any(self.state['autoRange']):
            return
            
        fractionVisible = self.state['autoRange'][:]
        for i in [0,1]:
            if type(fractionVisible[i]) is bool:
                fractionVisible[i] = 1.0
        cr = self.childrenBoundingRect(frac=fractionVisible)
        wp = cr.width() * 0.02
        hp = cr.height() * 0.02
        cr = cr.adjusted(-wp, -hp, wp, hp)
        
        if self.state['autoRange'][0] is not False:
            tr.setLeft(cr.left())
            tr.setRight(cr.right())
        if self.state['autoRange'][1] is not False:
            tr.setTop(cr.top())
            tr.setBottom(cr.bottom())
            
        self.setRange(tr, padding=0, disableAutoRange=False)
        
    def setXLink(self, view):
        self.linkView(self.XAxis, view)
        
    def setYLink(self, view):
        self.linkView(self.YAxis, view)
        
        
    def linkView(self, axis, view):
        """
        Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis.
        If view is None, the axis is left unlinked.
        """
        if isinstance(view, basestring):
            if view == '':
                view = None
            else:
                view = ViewBox.NamedViews[view]

        if hasattr(view, 'implements') and view.implements('ViewBoxWrapper'):
            view = view.getViewBox()

        ## used to connect/disconnect signals between a pair of views
        if axis == ViewBox.XAxis:
            signal = 'sigXRangeChanged'
            slot = self.linkedXChanged
        else:
            signal = 'sigYRangeChanged'
            slot = self.linkedYChanged


        oldLink = self.state['linkedViews'][axis]
        if oldLink is not None:
            getattr(oldLink, signal).disconnect(slot)
            
        self.state['linkedViews'][axis] = view
        
        if view is not None:
            getattr(view, signal).connect(slot)
            if view.autoRangeEnabled()[axis] is not False:
                self.enableAutoRange(axis, False)
                slot()
            else:
                if self.autoRangeEnabled()[axis] is False:
                    slot()
            
        self.sigStateChanged.emit(self)
        
    def blockLink(self, b):
        self.linksBlocked = b  ## prevents recursive plot-change propagation

    def linkedXChanged(self):
        ## called when x range of linked view has changed
        view = self.state['linkedViews'][0]
        self.linkedViewChanged(view, ViewBox.XAxis)

    def linkedYChanged(self):
        ## called when y range of linked view has changed
        view = self.state['linkedViews'][1]
        self.linkedViewChanged(view, ViewBox.YAxis)
        

    def linkedViewChanged(self, view, axis):
        if self.linksBlocked or view is None:
            return
        
        vr = view.viewRect()
        vg = view.screenGeometry()
        if vg is None:
            return
            
        sg = self.screenGeometry()
        
        view.blockLink(True)
        try:
            if axis == ViewBox.XAxis:
                overlap = min(sg.right(), vg.right()) - max(sg.left(), vg.left())
                if overlap < min(vg.width()/3, sg.width()/3):  ## if less than 1/3 of views overlap, 
                                                               ## then just replicate the view
                    x1 = vr.left()
                    x2 = vr.right()
                else:  ## views overlap; line them up
                    upp = float(vr.width()) / vg.width()
                    x1 = vr.left() + (sg.x()-vg.x()) * upp
                    x2 = x1 + sg.width() * upp
                self.enableAutoRange(ViewBox.XAxis, False)
                self.setXRange(x1, x2, padding=0)
            else:
                overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top())
                if overlap < min(vg.height()/3, sg.height()/3):  ## if less than 1/3 of views overlap, 
                                                               ## then just replicate the view
                    x1 = vr.top()
                    x2 = vr.bottom()
                else:  ## views overlap; line them up
                    upp = float(vr.height()) / vg.height()
                    x1 = vr.top() + (sg.y()-vg.y()) * upp
                    x2 = x1 + sg.height() * upp
                self.enableAutoRange(ViewBox.YAxis, False)
                self.setYRange(x1, x2, padding=0)
        finally:
            view.blockLink(False)
        
        
    def screenGeometry(self):
        """return the screen geometry of the viewbox"""
        v = self.getViewWidget()
        if v is None:
            return None
        b = self.sceneBoundingRect()
        wr = v.mapFromScene(b).boundingRect()
        pos = v.mapToGlobal(v.pos())
        wr.adjust(pos.x(), pos.y(), pos.x(), pos.y())
        return wr
        
    

    def itemsChanged(self):
        ## called when items are added/removed from self.childGroup
        self.updateAutoRange()
        
    def itemBoundsChanged(self, item):
        self.updateAutoRange()

    def invertY(self, b=True):
        """
        By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis.
        """
        self.state['yInverted'] = b
        self.updateMatrix()
        self.sigStateChanged.emit(self)
        
    def setAspectLocked(self, lock=True, ratio=1):
        """
        If the aspect ratio is locked, view scaling must always preserve the aspect ratio.
        By default, the ratio is set to 1; x and y both have the same scaling.
        This ratio can be overridden (width/height), or use None to lock in the current ratio.
        """
        if not lock:
            self.state['aspectLocked'] = False
        else:
            vr = self.viewRect()
            currentRatio = vr.width() / vr.height()
            if ratio is None:
                ratio = currentRatio
            self.state['aspectLocked'] = ratio
            if ratio != currentRatio:  ## If this would change the current range, do that now
                #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1])
                self.updateMatrix()
        self.sigStateChanged.emit(self)
        
    def childTransform(self):
        """
        Return the transform that maps from child(item in the childGroup) coordinates to local coordinates.
        (This maps from inside the viewbox to outside)
        """ 
        m = self.childGroup.transform()
        #m1 = QtGui.QTransform()
        #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y())
        return m #*m1

    def mapToView(self, obj):
        """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox"""
        m = self.childTransform().inverted()[0]
        return m.map(obj)

    def mapFromView(self, obj):
        """Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox"""
        m = self.childTransform()
        return m.map(obj)

    def mapSceneToView(self, obj):
        """Maps from scene coordinates to the coordinate system displayed inside the ViewBox"""
        return self.mapToView(self.mapFromScene(obj))

    def mapViewToScene(self, obj):
        """Maps from the coordinate system displayed inside the ViewBox to scene coordinates"""
        return self.mapToScene(self.mapFromView(obj))
    
    def mapFromItemToView(self, item, obj):
        return self.childGroup.mapFromItem(item, obj)
        #return self.mapSceneToView(item.mapToScene(obj))

    def mapFromViewToItem(self, item, obj):
        return self.childGroup.mapToItem(item, obj)
        #return item.mapFromScene(self.mapViewToScene(obj))

    def itemBoundingRect(self, item):
        """Return the bounding rect of the item in view coordinates"""
        return self.mapSceneToView(item.sceneBoundingRect()).boundingRect()
    
    #def viewScale(self):
        #vr = self.viewRect()
        ##print "viewScale:", self.range
        #xd = vr.width()
        #yd = vr.height()
        #if xd == 0 or yd == 0:
            #print "Warning: 0 range in view:", xd, yd
            #return np.array([1,1])
        
        ##cs = self.canvas().size()
        #cs = self.boundingRect()
        #scale = np.array([cs.width() / xd, cs.height() / yd])
        ##print "view scale:", scale
        #return scale

    def wheelEvent(self, ev, axis=None):
        mask = np.array(self.state['mouseEnabled'], dtype=np.float)
        if axis is not None and axis >= 0 and axis < len(mask):
            mv = mask[axis]
            mask[:] = 0
            mask[axis] = mv
        s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor
        
        center = Point(self.childGroup.transform().inverted()[0].map(ev.pos()))
        #center = ev.pos()
        
        self.scaleBy(s, center)
        self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
        ev.accept()

        
    def mouseClickEvent(self, ev):
        if ev.button() == QtCore.Qt.RightButton:
            ev.accept()
            self.raiseContextMenu(ev)
    
    def raiseContextMenu(self, ev):
        #print "viewbox.raiseContextMenu called."
        
        #menu = self.getMenu(ev)
        menu = self.getMenu(ev)
        self.scene().addParentContextMenus(self, menu, ev)
        #print "2:", [str(a.text()) for a in self.menu.actions()]
        pos = ev.screenPos()
        #pos2 = ev.scenePos()
        #print "3:", [str(a.text()) for a in self.menu.actions()]
        #self.sigActionPositionChanged.emit(pos2)

        menu.popup(QtCore.QPoint(pos.x(), pos.y()))
        #print "4:", [str(a.text()) for a in self.menu.actions()]
        
    def getMenu(self, ev):
        self._menuCopy = self.menu.copy()  ## temporary storage to prevent menu disappearing
        return self._menuCopy
        
    def getContextMenus(self, event):
        return self.menu.subMenus()
        #return [self.getMenu(event)]
        

    def mouseDragEvent(self, ev, axis=None):
        ## if axis is specified, event will only affect that axis.
        ev.accept()  ## we accept all buttons
        
        pos = ev.pos()
        lastPos = ev.lastPos()
        dif = pos - lastPos
        dif = dif * -1

        ## Ignore axes if mouse is disabled
        mask = np.array(self.state['mouseEnabled'], dtype=np.float)
        if axis is not None:
            mask[1-axis] = 0.0

        ## Scale or translate based on mouse button
        if ev.button() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton):
            if self.state['mouseMode'] == ViewBox.RectMode:
                if ev.isFinish():  ## This is the final move in the drag; change the view scale now
                    #print "finish"
                    self.rbScaleBox.hide()
                    #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos))
                    ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos))
                    ax = self.childGroup.mapRectFromParent(ax)
                    self.showAxRect(ax)
                    self.axHistoryPointer += 1
                    self.axHistory = self.axHistory[:self.axHistoryPointer] + [ax]
                else:
                    ## update shape of scale box
                    self.updateScaleBox(ev.buttonDownPos(), ev.pos())
            else:
                tr = dif*mask
                tr = self.mapToView(tr) - self.mapToView(Point(0,0))
                self.translateBy(tr)
                self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
        elif ev.button() & QtCore.Qt.RightButton:
            #print "vb.rightDrag"
            if self.state['aspectLocked'] is not False:
                mask[0] = 0
            
            dif = ev.screenPos() - ev.lastScreenPos()
            dif = np.array([dif.x(), dif.y()])
            dif[0] *= -1
            s = ((mask * 0.02) + 1) ** dif
            center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton)))
            #center = Point(ev.buttonDownPos(QtCore.Qt.RightButton))
            self.scaleBy(s, center)
            self.sigRangeChangedManually.emit(self.state['mouseEnabled'])

    def keyPressEvent(self, ev):
        """
        This routine should capture key presses in the current view box.
        Key presses are used only when mouse mode is RectMode
        The following events are implemented:
        ctrl-A : zooms out to the default "full" view of the plot
        ctrl-+ : moves forward in the zooming stack (if it exists)
        ctrl-- : moves backward in the zooming stack (if it exists)
         
        """
        #print ev.key()
        #print 'I intercepted a key press, but did not accept it'
        
        ## not implemented yet ?
        #self.keypress.sigkeyPressEvent.emit()
        
        ev.accept()
        if ev.text() == '-':
            self.scaleHistory(-1)
        elif ev.text() in ['+', '=']:
            self.scaleHistory(1)
        elif ev.key() == QtCore.Qt.Key_Backspace:
            self.scaleHistory(len(self.axHistory))
        else:
            ev.ignore()

    def scaleHistory(self, d):
        ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d))
        if ptr != self.axHistoryPointer:
            self.axHistoryPointer = ptr
            self.showAxRect(self.axHistory[ptr])
            

    def updateScaleBox(self, p1, p2):
        r = QtCore.QRectF(p1, p2)
        r = self.childGroup.mapRectFromParent(r)
        self.rbScaleBox.setPos(r.topLeft())
        self.rbScaleBox.resetTransform()
        self.rbScaleBox.scale(r.width(), r.height())
        self.rbScaleBox.show()

    def showAxRect(self, ax):
        self.setRange(ax.normalized()) # be sure w, h are correct coordinates
        self.sigRangeChangedManually.emit(self.state['mouseEnabled'])

    #def mouseRect(self):
        #vs = self.viewScale()
        #vr = self.state['viewRange']
        ## Convert positions from screen (view) pixel coordinates to axis coordinates 
        #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]),
            #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1])
        #return(ax)

    def allChildren(self, item=None):
        """Return a list of all children and grandchildren of this ViewBox"""
        if item is None:
            item = self.childGroup
        
        children = [item]
        for ch in item.childItems():
            children.extend(self.allChildren(ch))
        return children
        
        
        
    def childrenBoundingRect(self, frac=None):
        """Return the bounding range of all children.
        [[xmin, xmax], [ymin, ymax]]
        Values may be None if there are no specific bounds for an axis.
        """
        
        #items = self.allChildren()
        items = self.addedItems
        
        #if item is None:
            ##print "children bounding rect:"
            #item = self.childGroup
            
        range = [None, None]
            
        for item in items:
            if not item.isVisible():
                continue
        
            #print "=========", item
            useX = True
            useY = True
            if hasattr(item, 'dataBounds'):
                if frac is None:
                    frac = (1.0, 1.0)
                xr = item.dataBounds(0, frac=frac[0])
                yr = item.dataBounds(1, frac=frac[1])
                if xr is None or xr == (None, None):
                    useX = False
                    xr = (0,0)
                if yr is None or yr == (None, None):
                    useY = False
                    yr = (0,0)

                bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
                #print "   item real:", bounds
            else:
                if int(item.flags() & item.ItemHasNoContents) > 0:
                    continue
                    #print "   empty"
                else:
                    bounds = item.boundingRect()
                    #bounds = [[item.left(), item.top()], [item.right(), item.bottom()]]
                #print "   item:", bounds
            #bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0])
            bounds = self.mapFromItemToView(item, bounds).boundingRect()
            #print "    ", bounds
            
            
            if not any([useX, useY]):
                continue
            
            if useX != useY:  ##   !=  means  xor
                ang = item.transformAngle()
                if ang == 0 or ang == 180:
                    pass
                elif ang == 90 or ang == 270:
                    tmp = useX
                    useY = useX
                    useX = tmp
                else:
                    continue  ## need to check for item rotations and decide how best to apply this boundary. 
            
            
            if useY:
                if range[1] is not None:
                    range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])]
                    #bounds.setTop(min(bounds.top(), chb.top()))
                    #bounds.setBottom(max(bounds.bottom(), chb.bottom()))
                else:
                    range[1] = [bounds.top(), bounds.bottom()]
                    #bounds.setTop(chb.top())
                    #bounds.setBottom(chb.bottom())
            if useX:
                if range[0] is not None:
                    range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])]
                    #bounds.setLeft(min(bounds.left(), chb.left()))
                    #bounds.setRight(max(bounds.right(), chb.right()))
                else:
                    range[0] = [bounds.left(), bounds.right()]
                    #bounds.setLeft(chb.left())
                    #bounds.setRight(chb.right())
        
        tr = self.targetRange()
        if range[0] is None:
            range[0] = tr[0]
        if range[1] is None:
            range[1] = tr[1]
            
        bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0])
        return bounds
            
        

    def updateMatrix(self, changed=None):
        if changed is None:
            changed = [False, False]
        #print "udpateMatrix:"
        #print "  range:", self.range
        tr = self.targetRect()
        bounds = self.rect() #boundingRect()
        #print bounds
        
        ## set viewRect, given targetRect and possibly aspect ratio constraint
        if self.state['aspectLocked'] is False or bounds.height() == 0:
            self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
        else:
            viewRatio = bounds.width() / bounds.height()
            targetRatio = self.state['aspectLocked'] * tr.width() / tr.height()
            if targetRatio > viewRatio:  
                ## target is wider than view
                dy = 0.5 * (tr.width() / (self.state['aspectLocked'] * viewRatio) - tr.height())
                if dy != 0:
                    changed[1] = True
                self.state['viewRange'] = [self.state['targetRange'][0][:], [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]]
            else:
                dx = 0.5 * (tr.height() * viewRatio * self.state['aspectLocked'] - tr.width())
                if dx != 0:
                    changed[0] = True
                self.state['viewRange'] = [[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], self.state['targetRange'][1][:]]
        
        vr = self.viewRect()
        #print "  bounds:", bounds
        if vr.height() == 0 or vr.width() == 0:
            return
        scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height())
        if not self.state['yInverted']:
            scale = scale * Point(1, -1)
        m = QtGui.QTransform()
        
        ## First center the viewport at 0
        #self.childGroup.resetTransform()
        #self.resetTransform()
        #center = self.transform().inverted()[0].map(bounds.center())
        center = bounds.center()
        #print "  transform to center:", center
        #if self.state['yInverted']:
            #m.translate(center.x(), -center.y())
            #print "  inverted; translate", center.x(), center.y()
        #else:
        m.translate(center.x(), center.y())
            #print "  not inverted; translate", center.x(), -center.y()
            
        ## Now scale and translate properly
        m.scale(scale[0], scale[1])
        st = Point(vr.center())
        #st = translate
        m.translate(-st[0], -st[1])
        
        self.childGroup.setTransform(m)
        #self.setTransform(m)
        #self.prepareGeometryChange()
        
        #self.currentScale = scale
        
        if changed[0]:
            self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
        if changed[1]:
            self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
        if any(changed):
            self.sigRangeChanged.emit(self, self.state['viewRange'])

    def paint(self, p, opt, widget):
        if self.border is not None:
            bounds = self.shape()
            p.setPen(self.border)
            #p.fillRect(bounds, QtGui.QColor(0, 0, 0))
            p.drawPath(bounds)

    def saveSvg(self):
        pass
        
    def saveImage(self):
        pass

    def savePrint(self):
        printer = QtGui.QPrinter()
        if QtGui.QPrintDialog(printer).exec_() == QtGui.QDialog.Accepted:
            p = QtGui.QPainter(printer)
            p.setRenderHint(p.Antialiasing)
            self.scene().render(p)
            p.end()

    def updateViewLists(self):
        def cmpViews(a, b):
            wins = 100 * cmp(a.window() is self.window(), b.window() is self.window())
            alpha = cmp(a.name, b.name)
            return wins + alpha
            
        ## make a sorted list of all named views
        nv = ViewBox.NamedViews.values()
        nv.sort(cmpViews)
        
        if self in nv:
            nv.remove(self)
        names = [v.name for v in nv]
        self.menu.setViewList(names)

    @staticmethod
    def updateAllViewLists():
        for v in ViewBox.AllViews:
            v.updateViewLists()
Beispiel #4
0
class ViewBox(GraphicsWidget):
    """
    **Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>`
    
    Box that allows internal scaling/panning of children by mouse drag. 
    This class is usually created automatically as part of a :class:`PlotItem <pyqtgraph.PlotItem>` or :class:`Canvas <pyqtgraph.canvas.Canvas>` or with :func:`GraphicsLayout.addViewBox() <pyqtgraph.GraphicsLayout.addViewBox>`.
    
    Features:
    
        - Scaling contents by mouse or auto-scale when contents change
        - View linking--multiple views display the same data ranges
        - Configurable by context menu
        - Item coordinate mapping methods
    
    Not really compatible with GraphicsView having the same functionality.
    """

    sigYRangeChanged = QtCore.Signal(object, object)
    sigXRangeChanged = QtCore.Signal(object, object)
    sigRangeChangedManually = QtCore.Signal(object)
    sigRangeChanged = QtCore.Signal(object, object)
    #sigActionPositionChanged = QtCore.Signal(object)
    sigStateChanged = QtCore.Signal(object)

    ## mouse modes
    PanMode = 3
    RectMode = 1

    ## axes
    XAxis = 0
    YAxis = 1
    XYAxes = 2

    ## for linking views together
    NamedViews = weakref.WeakValueDictionary()  # name: ViewBox
    AllViews = weakref.WeakKeyDictionary()  # ViewBox: None

    def __init__(self,
                 parent=None,
                 border=None,
                 lockAspect=False,
                 enableMouse=True,
                 invertY=False,
                 name=None):
        """
        =============  =============================================================
        **Arguments**
        *parent*       (QGraphicsWidget) Optional parent widget
        *border*       (QPen) Do draw a border around the view, give any 
                       single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
        *lockAspect*   (False or float) The aspect ratio to lock the view 
                       coorinates to. (or False to allow the ratio to change)
        *enableMouse*  (bool) Whether mouse can be used to scale/pan the view 
        *invertY*      (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
        =============  =============================================================
        """

        GraphicsWidget.__init__(self, parent)
        self.name = None
        self.linksBlocked = False
        self.addedItems = []
        #self.gView = view
        #self.showGrid = showGrid

        self.state = {

            ## separating targetRange and viewRange allows the view to be resized
            ## while keeping all previously viewed contents visible
            'targetRange':
            [[0, 1],
             [0,
              1]],  ## child coord. range visible [[xmin, xmax], [ymin, ymax]]
            'viewRange': [[0, 1], [0, 1]],  ## actual range viewed
            'yInverted':
            invertY,
            'aspectLocked':
            False,  ## False if aspect is unlocked, otherwise float specifies the locked ratio.
            'autoRange': [True, True],  ## False if auto range is disabled, 
            ## otherwise float gives the fraction of data that is visible
            'linkedViews': [None, None],
            'mouseEnabled': [enableMouse, enableMouse],
            'mouseMode':
            ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else
            ViewBox.RectMode,
            'wheelScaleFactor':
            -1.0 / 8.0,
        }

        #self.exportMethods = collections.OrderedDict([
        #('SVG', self.saveSvg),
        #('Image', self.saveImage),
        #('Print', self.savePrint),
        #])

        self.setFlag(self.ItemClipsChildrenToShape)
        self.setFlag(self.ItemIsFocusable,
                     True)  ## so we can receive key presses

        ## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
        ## this is a workaround for a Qt + OpenGL but that causes improper clipping
        ## https://bugreports.qt.nokia.com/browse/QTBUG-23723
        self.childGroup = ChildGroup(self)
        self.childGroup.sigItemsChanged.connect(self.itemsChanged)

        #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
        # this also enables capture of keyPressEvents.

        ## Make scale box that is shown when dragging on the view
        self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
        self.rbScaleBox.setPen(fn.mkPen((255, 0, 0), width=1))
        self.rbScaleBox.setBrush(fn.mkBrush(255, 255, 0, 100))
        self.rbScaleBox.hide()
        self.addItem(self.rbScaleBox)

        self.axHistory = []  # maintain a history of zoom locations
        self.axHistoryPointer = -1  # pointer into the history. Allows forward/backward movement, not just "undo"

        self.setZValue(-100)
        self.setSizePolicy(
            QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
                              QtGui.QSizePolicy.Expanding))

        self.setAspectLocked(lockAspect)

        self.border = fn.mkPen(border)
        self.menu = ViewBoxMenu(self)

        self.register(name)
        if name is None:
            self.updateViewLists()

    def register(self, name):
        """
        Add this ViewBox to the registered list of views. 
        *name* will appear in the drop-down lists for axis linking in all other views.
        The same can be accomplished by initializing the ViewBox with the *name* attribute.
        """
        ViewBox.AllViews[self] = None
        if self.name is not None:
            del ViewBox.NamedViews[self.name]
        self.name = name
        if name is not None:
            ViewBox.NamedViews[name] = self
            ViewBox.updateAllViewLists()

    def unregister(self):
        """
        Remove this ViewBox forom the list of linkable views. (see :func:`register() <pyqtgraph.ViewBox.register>`)
        """
        del ViewBox.AllViews[self]
        if self.name is not None:
            del ViewBox.NamedViews[self.name]

    def close(self):
        self.unregister()

    def implements(self, interface):
        return interface == 'ViewBox'

    def getState(self, copy=True):
        state = self.state.copy()
        state['linkedViews'] = [(None if v is None else v.name)
                                for v in state['linkedViews']]
        if copy:
            return deepcopy(self.state)
        else:
            return self.state

    def setState(self, state):
        state = state.copy()
        self.setXLink(state['linkedViews'][0])
        self.setYLink(state['linkedViews'][1])
        del state['linkedViews']

        self.state.update(state)
        self.updateMatrix()
        self.sigStateChanged.emit(self)

    def setMouseMode(self, mode):
        """
        Set the mouse interaction mode. *mode* must be either ViewBox.PanMode or ViewBox.RectMode.
        In PanMode, the left mouse button pans the view and the right button scales.
        In RectMode, the left button draws a rectangle which updates the visible region (this mode is more suitable for single-button mice)
        """
        if mode not in [ViewBox.PanMode, ViewBox.RectMode]:
            raise Exception("Mode must be ViewBox.PanMode or ViewBox.RectMode")
        self.state['mouseMode'] = mode
        self.sigStateChanged.emit(self)

    #def toggleLeftAction(self, act):  ## for backward compatibility
    #if act.text() is 'pan':
    #self.setLeftButtonAction('pan')
    #elif act.text() is 'zoom':
    #self.setLeftButtonAction('rect')

    def setLeftButtonAction(self, mode='rect'):  ## for backward compatibility
        if mode.lower() == 'rect':
            self.setMouseMode(ViewBox.RectMode)
        elif mode.lower() == 'pan':
            self.setMouseMode(ViewBox.PanMode)
        else:
            raise Exception(
                'graphicsItems:ViewBox:setLeftButtonAction: unknown mode = %s (Options are "pan" and "rect")'
                % mode)

    def innerSceneItem(self):
        return self.childGroup

    def setMouseEnabled(self, x=None, y=None):
        """
        Set whether each axis is enabled for mouse interaction. *x*, *y* arguments must be True or False.
        This allows the user to pan/scale one axis of the view while leaving the other axis unchanged.
        """
        if x is not None:
            self.state['mouseEnabled'][0] = x
        if y is not None:
            self.state['mouseEnabled'][1] = y
        self.sigStateChanged.emit(self)

    def mouseEnabled(self):
        return self.state['mouseEnabled'][:]

    def addItem(self, item, ignoreBounds=False):
        """
        Add a QGraphicsItem to this view. The view will include this item when determining how to set its range
        automatically unless *ignoreBounds* is True.
        """
        if item.zValue() < self.zValue():
            item.setZValue(self.zValue() + 1)
        item.setParentItem(self.childGroup)
        if not ignoreBounds:
            self.addedItems.append(item)
        self.updateAutoRange()
        #print "addItem:", item, item.boundingRect()

    def removeItem(self, item):
        """Remove an item from this view."""
        try:
            self.addedItems.remove(item)
        except:
            pass
        self.scene().removeItem(item)
        self.updateAutoRange()

    def resizeEvent(self, ev):
        #self.setRange(self.range, padding=0)
        #self.updateAutoRange()
        self.updateMatrix()
        self.sigStateChanged.emit(self)
        #self.linkedXChanged()
        #self.linkedYChanged()

    def viewRange(self):
        """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]"""
        return [x[:] for x in self.state['viewRange']]  ## return copy

    def viewRect(self):
        """Return a QRectF bounding the region visible within the ViewBox"""
        try:
            vr0 = self.state['viewRange'][0]
            vr1 = self.state['viewRange'][1]
            return QtCore.QRectF(vr0[0], vr1[0], vr0[1] - vr0[0],
                                 vr1[1] - vr1[0])
        except:
            print "make qrectf failed:", self.state['viewRange']
            raise

    #def viewportTransform(self):
    ##return self.itemTransform(self.childGroup)[0]
    #return self.childGroup.itemTransform(self)[0]

    def targetRange(self):
        return [x[:] for x in self.state['targetRange']]  ## return copy

    def targetRect(self):
        """
        Return the region which has been requested to be visible. 
        (this is not necessarily the same as the region that is *actually* visible--
        resizing and aspect ratio constraints can cause targetRect() and viewRect() to differ)
        """
        try:
            tr0 = self.state['targetRange'][0]
            tr1 = self.state['targetRange'][1]
            return QtCore.QRectF(tr0[0], tr1[0], tr0[1] - tr0[0],
                                 tr1[1] - tr1[0])
        except:
            print "make qrectf failed:", self.state['targetRange']
            raise

    def setRange(self,
                 rect=None,
                 xRange=None,
                 yRange=None,
                 padding=0.02,
                 update=True,
                 disableAutoRange=True):
        """
        Set the visible range of the ViewBox.
        Must specify at least one of *range*, *xRange*, or *yRange*. 
        
        ============= =====================================================================
        **Arguments**
        *rect*        (QRectF) The full range that should be visible in the view box.
        *xRange*      (min,max) The range that should be visible along the x-axis.
        *yRange*      (min,max) The range that should be visible along the y-axis.
        *padding*     (float) Expand the view by a fraction of the requested range. 
                      By default, this value is 0.02 (2%)
        ============= =====================================================================
        
        """
        changes = {}

        if rect is not None:
            changes = {
                0: [rect.left(), rect.right()],
                1: [rect.top(), rect.bottom()]
            }
        if xRange is not None:
            changes[0] = xRange
        if yRange is not None:
            changes[1] = yRange

        if len(changes) == 0:
            raise Exception(
                "Must specify at least one of rect, xRange, or yRange.")

        changed = [False, False]
        for ax, range in changes.iteritems():
            mn = min(range)
            mx = max(range)
            if mn == mx:  ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale.
                dy = self.state['viewRange'][ax][1] - self.state['viewRange'][
                    ax][0]
                if dy == 0:
                    dy = 1
                mn -= dy * 0.5
                mx += dy * 0.5
                padding = 0.0
            if any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])):
                raise Exception("Not setting range [%s, %s]" %
                                (str(mn), str(mx)))

            p = (mx - mn) * padding
            mn -= p
            mx += p

            if self.state['targetRange'][ax] != [mn, mx]:
                self.state['targetRange'][ax] = [mn, mx]
                changed[ax] = True

        if any(changed) and disableAutoRange:
            if all(changed):
                ax = ViewBox.XYAxes
            elif changed[0]:
                ax = ViewBox.XAxis
            elif changed[1]:
                ax = ViewBox.YAxis
            self.enableAutoRange(ax, False)

        self.sigStateChanged.emit(self)

        if update:
            self.updateMatrix(changed)

        for ax, range in changes.iteritems():
            link = self.state['linkedViews'][ax]
            if link is not None:
                link.linkedViewChanged(self, ax)

    def setYRange(self, min, max, padding=0.02, update=True):
        """
        Set the visible Y range of the view to [*min*, *max*]. 
        The *padding* argument causes the range to be set larger by the fraction specified.
        """
        self.setRange(yRange=[min, max], update=update, padding=padding)

    def setXRange(self, min, max, padding=0.02, update=True):
        """
        Set the visible X range of the view to [*min*, *max*]. 
        The *padding* argument causes the range to be set larger by the fraction specified.
        """
        self.setRange(xRange=[min, max], update=update, padding=padding)

    def autoRange(self, padding=0.02):
        """
        Set the range of the view box to make all children visible.
        Note that this is not the same as enableAutoRange, which causes the view to 
        automatically auto-range whenever its contents are changed.
        """
        bounds = self.childrenBoundingRect()
        if bounds is not None:
            self.setRange(bounds, padding=padding)

    def scaleBy(self, s, center=None):
        """
        Scale by *s* around given center point (or center of view).
        *s* may be a Point or tuple (x, y)
        """
        scale = Point(s)
        if self.state['aspectLocked'] is not False:
            scale[0] = self.state['aspectLocked'] * scale[1]

        vr = self.targetRect()
        if center is None:
            center = Point(vr.center())
        else:
            center = Point(center)

        tl = center + (vr.topLeft() - center) * scale
        br = center + (vr.bottomRight() - center) * scale

        self.setRange(QtCore.QRectF(tl, br), padding=0)

    def translateBy(self, t):
        """
        Translate the view by *t*, which may be a Point or tuple (x, y).
        """
        t = Point(t)
        #if viewCoords:  ## scale from pixels
        #o = self.mapToView(Point(0,0))
        #t = self.mapToView(t) - o

        vr = self.targetRect()
        self.setRange(vr.translated(t), padding=0)

    def enableAutoRange(self, axis=None, enable=True):
        """
        Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both.
        When enabled, the axis will automatically rescale when items are added/removed or change their shape.
        The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should
        be visible (this only works with items implementing a dataRange method, such as PlotDataItem).
        """
        #print "autorange:", axis, enable
        #if not enable:
        #import traceback
        #traceback.print_stack()
        if enable is True:
            enable = 1.0

        if axis is None:
            axis = ViewBox.XYAxes

        if axis == ViewBox.XYAxes or axis == 'xy':
            self.state['autoRange'][0] = enable
            self.state['autoRange'][1] = enable
        elif axis == ViewBox.XAxis or axis == 'x':
            self.state['autoRange'][0] = enable
        elif axis == ViewBox.YAxis or axis == 'y':
            self.state['autoRange'][1] = enable
        else:
            raise Exception(
                'axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.'
            )

        if enable:
            self.updateAutoRange()
        self.sigStateChanged.emit(self)

    def disableAutoRange(self, axis=None):
        """Disables auto-range. (See enableAutoRange)"""
        self.enableAutoRange(axis, enable=False)

    def autoRangeEnabled(self):
        return self.state['autoRange'][:]

    def updateAutoRange(self):
        tr = self.viewRect()
        if not any(self.state['autoRange']):
            return

        fractionVisible = self.state['autoRange'][:]
        for i in [0, 1]:
            if type(fractionVisible[i]) is bool:
                fractionVisible[i] = 1.0
        cr = self.childrenBoundingRect(frac=fractionVisible)
        wp = cr.width() * 0.02
        hp = cr.height() * 0.02
        cr = cr.adjusted(-wp, -hp, wp, hp)

        if self.state['autoRange'][0] is not False:
            tr.setLeft(cr.left())
            tr.setRight(cr.right())
        if self.state['autoRange'][1] is not False:
            tr.setTop(cr.top())
            tr.setBottom(cr.bottom())

        self.setRange(tr, padding=0, disableAutoRange=False)

    def setXLink(self, view):
        """Link this view's X axis to another view. (see LinkView)"""
        self.linkView(self.XAxis, view)

    def setYLink(self, view):
        """Link this view's Y axis to another view. (see LinkView)"""
        self.linkView(self.YAxis, view)

    def linkView(self, axis, view):
        """
        Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis.
        If view is None, the axis is left unlinked.
        """
        if isinstance(view, basestring):
            if view == '':
                view = None
            else:
                view = ViewBox.NamedViews[view]

        if hasattr(view, 'implements') and view.implements('ViewBoxWrapper'):
            view = view.getViewBox()

        ## used to connect/disconnect signals between a pair of views
        if axis == ViewBox.XAxis:
            signal = 'sigXRangeChanged'
            slot = self.linkedXChanged
        else:
            signal = 'sigYRangeChanged'
            slot = self.linkedYChanged

        oldLink = self.state['linkedViews'][axis]
        if oldLink is not None:
            getattr(oldLink, signal).disconnect(slot)

        self.state['linkedViews'][axis] = view

        if view is not None:
            getattr(view, signal).connect(slot)
            if view.autoRangeEnabled()[axis] is not False:
                self.enableAutoRange(axis, False)
                slot()
            else:
                if self.autoRangeEnabled()[axis] is False:
                    slot()

        self.sigStateChanged.emit(self)

    def blockLink(self, b):
        self.linksBlocked = b  ## prevents recursive plot-change propagation

    def linkedXChanged(self):
        ## called when x range of linked view has changed
        view = self.state['linkedViews'][0]
        self.linkedViewChanged(view, ViewBox.XAxis)

    def linkedYChanged(self):
        ## called when y range of linked view has changed
        view = self.state['linkedViews'][1]
        self.linkedViewChanged(view, ViewBox.YAxis)

    def linkedViewChanged(self, view, axis):
        if self.linksBlocked or view is None:
            return

        vr = view.viewRect()
        vg = view.screenGeometry()
        if vg is None:
            return

        sg = self.screenGeometry()

        view.blockLink(True)
        try:
            if axis == ViewBox.XAxis:
                overlap = min(sg.right(), vg.right()) - max(
                    sg.left(), vg.left())
                if overlap < min(
                        vg.width() / 3,
                        sg.width() / 3):  ## if less than 1/3 of views overlap,
                    ## then just replicate the view
                    x1 = vr.left()
                    x2 = vr.right()
                else:  ## views overlap; line them up
                    upp = float(vr.width()) / vg.width()
                    x1 = vr.left() + (sg.x() - vg.x()) * upp
                    x2 = x1 + sg.width() * upp
                self.enableAutoRange(ViewBox.XAxis, False)
                self.setXRange(x1, x2, padding=0)
            else:
                overlap = min(sg.bottom(), vg.bottom()) - max(
                    sg.top(), vg.top())
                if overlap < min(vg.height() / 3,
                                 sg.height() /
                                 3):  ## if less than 1/3 of views overlap,
                    ## then just replicate the view
                    x1 = vr.top()
                    x2 = vr.bottom()
                else:  ## views overlap; line them up
                    upp = float(vr.height()) / vg.height()
                    x1 = vr.top() + (sg.y() - vg.y()) * upp
                    x2 = x1 + sg.height() * upp
                self.enableAutoRange(ViewBox.YAxis, False)
                self.setYRange(x1, x2, padding=0)
        finally:
            view.blockLink(False)

    def screenGeometry(self):
        """return the screen geometry of the viewbox"""
        v = self.getViewWidget()
        if v is None:
            return None
        b = self.sceneBoundingRect()
        wr = v.mapFromScene(b).boundingRect()
        pos = v.mapToGlobal(v.pos())
        wr.adjust(pos.x(), pos.y(), pos.x(), pos.y())
        return wr

    def itemsChanged(self):
        ## called when items are added/removed from self.childGroup
        self.updateAutoRange()

    def itemBoundsChanged(self, item):
        self.updateAutoRange()

    def invertY(self, b=True):
        """
        By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis.
        """
        self.state['yInverted'] = b
        self.updateMatrix()
        self.sigStateChanged.emit(self)

    def setAspectLocked(self, lock=True, ratio=1):
        """
        If the aspect ratio is locked, view scaling must always preserve the aspect ratio.
        By default, the ratio is set to 1; x and y both have the same scaling.
        This ratio can be overridden (width/height), or use None to lock in the current ratio.
        """
        if not lock:
            self.state['aspectLocked'] = False
        else:
            vr = self.viewRect()
            currentRatio = vr.width() / vr.height()
            if ratio is None:
                ratio = currentRatio
            self.state['aspectLocked'] = ratio
            if ratio != currentRatio:  ## If this would change the current range, do that now
                #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1])
                self.updateMatrix()
        self.sigStateChanged.emit(self)

    def childTransform(self):
        """
        Return the transform that maps from child(item in the childGroup) coordinates to local coordinates.
        (This maps from inside the viewbox to outside)
        """
        m = self.childGroup.transform()
        #m1 = QtGui.QTransform()
        #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y())
        return m  #*m1

    def mapToView(self, obj):
        """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox"""
        m = self.childTransform().inverted()[0]
        return m.map(obj)

    def mapFromView(self, obj):
        """Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox"""
        m = self.childTransform()
        return m.map(obj)

    def mapSceneToView(self, obj):
        """Maps from scene coordinates to the coordinate system displayed inside the ViewBox"""
        return self.mapToView(self.mapFromScene(obj))

    def mapViewToScene(self, obj):
        """Maps from the coordinate system displayed inside the ViewBox to scene coordinates"""
        return self.mapToScene(self.mapFromView(obj))

    def mapFromItemToView(self, item, obj):
        """Maps *obj* from the local coordinate system of *item* to the view coordinates"""
        return self.childGroup.mapFromItem(item, obj)
        #return self.mapSceneToView(item.mapToScene(obj))

    def mapFromViewToItem(self, item, obj):
        """Maps *obj* from view coordinates to the local coordinate system of *item*."""
        return self.childGroup.mapToItem(item, obj)
        #return item.mapFromScene(self.mapViewToScene(obj))

    def itemBoundingRect(self, item):
        """Return the bounding rect of the item in view coordinates"""
        return self.mapSceneToView(item.sceneBoundingRect()).boundingRect()

    #def viewScale(self):
    #vr = self.viewRect()
    ##print "viewScale:", self.range
    #xd = vr.width()
    #yd = vr.height()
    #if xd == 0 or yd == 0:
    #print "Warning: 0 range in view:", xd, yd
    #return np.array([1,1])

    ##cs = self.canvas().size()
    #cs = self.boundingRect()
    #scale = np.array([cs.width() / xd, cs.height() / yd])
    ##print "view scale:", scale
    #return scale

    def wheelEvent(self, ev, axis=None):
        mask = np.array(self.state['mouseEnabled'], dtype=np.float)
        if axis is not None and axis >= 0 and axis < len(mask):
            mv = mask[axis]
            mask[:] = 0
            mask[axis] = mv
        s = ((mask * 0.02) + 1)**(ev.delta() * self.state['wheelScaleFactor']
                                  )  # actual scaling factor

        center = Point(self.childGroup.transform().inverted()[0].map(ev.pos()))
        #center = ev.pos()

        self.scaleBy(s, center)
        self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
        ev.accept()

    def mouseClickEvent(self, ev):
        if ev.button() == QtCore.Qt.RightButton:
            ev.accept()
            self.raiseContextMenu(ev)

    def raiseContextMenu(self, ev):
        #print "viewbox.raiseContextMenu called."

        #menu = self.getMenu(ev)
        menu = self.getMenu(ev)
        self.scene().addParentContextMenus(self, menu, ev)
        #print "2:", [str(a.text()) for a in self.menu.actions()]
        pos = ev.screenPos()
        #pos2 = ev.scenePos()
        #print "3:", [str(a.text()) for a in self.menu.actions()]
        #self.sigActionPositionChanged.emit(pos2)

        menu.popup(QtCore.QPoint(pos.x(), pos.y()))
        #print "4:", [str(a.text()) for a in self.menu.actions()]

    def getMenu(self, ev):
        self._menuCopy = self.menu.copy(
        )  ## temporary storage to prevent menu disappearing
        return self._menuCopy

    def getContextMenus(self, event):
        return self.menu.subMenus()
        #return [self.getMenu(event)]

    def mouseDragEvent(self, ev, axis=None):
        ## if axis is specified, event will only affect that axis.
        ev.accept()  ## we accept all buttons

        pos = ev.pos()
        lastPos = ev.lastPos()
        dif = pos - lastPos
        dif = dif * -1

        ## Ignore axes if mouse is disabled
        mask = np.array(self.state['mouseEnabled'], dtype=np.float)
        if axis is not None:
            mask[1 - axis] = 0.0

        ## Scale or translate based on mouse button
        if ev.button() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton):
            if self.state['mouseMode'] == ViewBox.RectMode:
                if ev.isFinish(
                ):  ## This is the final move in the drag; change the view scale now
                    #print "finish"
                    self.rbScaleBox.hide()
                    #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos))
                    ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())),
                                       Point(pos))
                    ax = self.childGroup.mapRectFromParent(ax)
                    self.showAxRect(ax)
                    self.axHistoryPointer += 1
                    self.axHistory = self.axHistory[:self.axHistoryPointer] + [
                        ax
                    ]
                else:
                    ## update shape of scale box
                    self.updateScaleBox(ev.buttonDownPos(), ev.pos())
            else:
                tr = dif * mask
                tr = self.mapToView(tr) - self.mapToView(Point(0, 0))
                self.translateBy(tr)
                self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
        elif ev.button() & QtCore.Qt.RightButton:
            #print "vb.rightDrag"
            if self.state['aspectLocked'] is not False:
                mask[0] = 0

            dif = ev.screenPos() - ev.lastScreenPos()
            dif = np.array([dif.x(), dif.y()])
            dif[0] *= -1
            s = ((mask * 0.02) + 1)**dif
            center = Point(self.childGroup.transform().inverted()[0].map(
                ev.buttonDownPos(QtCore.Qt.RightButton)))
            #center = Point(ev.buttonDownPos(QtCore.Qt.RightButton))
            self.scaleBy(s, center)
            self.sigRangeChangedManually.emit(self.state['mouseEnabled'])

    def keyPressEvent(self, ev):
        """
        This routine should capture key presses in the current view box.
        Key presses are used only when mouse mode is RectMode
        The following events are implemented:
        ctrl-A : zooms out to the default "full" view of the plot
        ctrl-+ : moves forward in the zooming stack (if it exists)
        ctrl-- : moves backward in the zooming stack (if it exists)
         
        """
        #print ev.key()
        #print 'I intercepted a key press, but did not accept it'

        ## not implemented yet ?
        #self.keypress.sigkeyPressEvent.emit()

        ev.accept()
        if ev.text() == '-':
            self.scaleHistory(-1)
        elif ev.text() in ['+', '=']:
            self.scaleHistory(1)
        elif ev.key() == QtCore.Qt.Key_Backspace:
            self.scaleHistory(len(self.axHistory))
        else:
            ev.ignore()

    def scaleHistory(self, d):
        ptr = max(0, min(len(self.axHistory) - 1, self.axHistoryPointer + d))
        if ptr != self.axHistoryPointer:
            self.axHistoryPointer = ptr
            self.showAxRect(self.axHistory[ptr])

    def updateScaleBox(self, p1, p2):
        r = QtCore.QRectF(p1, p2)
        r = self.childGroup.mapRectFromParent(r)
        self.rbScaleBox.setPos(r.topLeft())
        self.rbScaleBox.resetTransform()
        self.rbScaleBox.scale(r.width(), r.height())
        self.rbScaleBox.show()

    def showAxRect(self, ax):
        self.setRange(ax.normalized())  # be sure w, h are correct coordinates
        self.sigRangeChangedManually.emit(self.state['mouseEnabled'])

    #def mouseRect(self):
    #vs = self.viewScale()
    #vr = self.state['viewRange']
    ## Convert positions from screen (view) pixel coordinates to axis coordinates
    #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]),
    #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1])
    #return(ax)

    def allChildren(self, item=None):
        """Return a list of all children and grandchildren of this ViewBox"""
        if item is None:
            item = self.childGroup

        children = [item]
        for ch in item.childItems():
            children.extend(self.allChildren(ch))
        return children

    def childrenBoundingRect(self, frac=None):
        """Return the bounding range of all children.
        [[xmin, xmax], [ymin, ymax]]
        Values may be None if there are no specific bounds for an axis.
        """

        #items = self.allChildren()
        items = self.addedItems

        #if item is None:
        ##print "children bounding rect:"
        #item = self.childGroup

        range = [None, None]

        for item in items:
            if not item.isVisible():
                continue

            #print "=========", item
            useX = True
            useY = True
            if hasattr(item, 'dataBounds'):
                if frac is None:
                    frac = (1.0, 1.0)
                xr = item.dataBounds(0, frac=frac[0])
                yr = item.dataBounds(1, frac=frac[1])
                if xr is None or xr == (None, None):
                    useX = False
                    xr = (0, 0)
                if yr is None or yr == (None, None):
                    useY = False
                    yr = (0, 0)

                bounds = QtCore.QRectF(xr[0], yr[0], xr[1] - xr[0],
                                       yr[1] - yr[0])
                #print "   item real:", bounds
            else:
                if int(item.flags() & item.ItemHasNoContents) > 0:
                    continue
                    #print "   empty"
                else:
                    bounds = item.boundingRect()
                    #bounds = [[item.left(), item.top()], [item.right(), item.bottom()]]
                #print "   item:", bounds
            #bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0])
            bounds = self.mapFromItemToView(item, bounds).boundingRect()
            #print "    ", bounds

            if not any([useX, useY]):
                continue

            if useX != useY:  ##   !=  means  xor
                ang = item.transformAngle()
                if ang == 0 or ang == 180:
                    pass
                elif ang == 90 or ang == 270:
                    tmp = useX
                    useY = useX
                    useX = tmp
                else:
                    continue  ## need to check for item rotations and decide how best to apply this boundary.

            if useY:
                if range[1] is not None:
                    range[1] = [
                        min(bounds.top(), range[1][0]),
                        max(bounds.bottom(), range[1][1])
                    ]
                    #bounds.setTop(min(bounds.top(), chb.top()))
                    #bounds.setBottom(max(bounds.bottom(), chb.bottom()))
                else:
                    range[1] = [bounds.top(), bounds.bottom()]
                    #bounds.setTop(chb.top())
                    #bounds.setBottom(chb.bottom())
            if useX:
                if range[0] is not None:
                    range[0] = [
                        min(bounds.left(), range[0][0]),
                        max(bounds.right(), range[0][1])
                    ]
                    #bounds.setLeft(min(bounds.left(), chb.left()))
                    #bounds.setRight(max(bounds.right(), chb.right()))
                else:
                    range[0] = [bounds.left(), bounds.right()]
                    #bounds.setLeft(chb.left())
                    #bounds.setRight(chb.right())

        tr = self.targetRange()
        if range[0] is None:
            range[0] = tr[0]
        if range[1] is None:
            range[1] = tr[1]

        bounds = QtCore.QRectF(range[0][0], range[1][0],
                               range[0][1] - range[0][0],
                               range[1][1] - range[1][0])
        return bounds

    def updateMatrix(self, changed=None):
        if changed is None:
            changed = [False, False]
        #print "udpateMatrix:"
        #print "  range:", self.range
        tr = self.targetRect()
        bounds = self.rect()  #boundingRect()
        #print bounds

        ## set viewRect, given targetRect and possibly aspect ratio constraint
        if self.state['aspectLocked'] is False or bounds.height() == 0:
            self.state['viewRange'] = [
                self.state['targetRange'][0][:],
                self.state['targetRange'][1][:]
            ]
        else:
            viewRatio = bounds.width() / bounds.height()
            targetRatio = self.state['aspectLocked'] * tr.width() / tr.height()
            if targetRatio > viewRatio:
                ## target is wider than view
                dy = 0.5 * (tr.width() /
                            (self.state['aspectLocked'] * viewRatio) -
                            tr.height())
                if dy != 0:
                    changed[1] = True
                self.state['viewRange'] = [
                    self.state['targetRange'][0][:],
                    [
                        self.state['targetRange'][1][0] - dy,
                        self.state['targetRange'][1][1] + dy
                    ]
                ]
            else:
                dx = 0.5 * (tr.height() * viewRatio *
                            self.state['aspectLocked'] - tr.width())
                if dx != 0:
                    changed[0] = True
                self.state['viewRange'] = [[
                    self.state['targetRange'][0][0] - dx,
                    self.state['targetRange'][0][1] + dx
                ], self.state['targetRange'][1][:]]

        vr = self.viewRect()
        #print "  bounds:", bounds
        if vr.height() == 0 or vr.width() == 0:
            return
        scale = Point(bounds.width() / vr.width(),
                      bounds.height() / vr.height())
        if not self.state['yInverted']:
            scale = scale * Point(1, -1)
        m = QtGui.QTransform()

        ## First center the viewport at 0
        #self.childGroup.resetTransform()
        #self.resetTransform()
        #center = self.transform().inverted()[0].map(bounds.center())
        center = bounds.center()
        #print "  transform to center:", center
        #if self.state['yInverted']:
        #m.translate(center.x(), -center.y())
        #print "  inverted; translate", center.x(), center.y()
        #else:
        m.translate(center.x(), center.y())
        #print "  not inverted; translate", center.x(), -center.y()

        ## Now scale and translate properly
        m.scale(scale[0], scale[1])
        st = Point(vr.center())
        #st = translate
        m.translate(-st[0], -st[1])

        self.childGroup.setTransform(m)
        #self.setTransform(m)
        #self.prepareGeometryChange()

        #self.currentScale = scale

        if changed[0]:
            self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
        if changed[1]:
            self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
        if any(changed):
            self.sigRangeChanged.emit(self, self.state['viewRange'])

    def paint(self, p, opt, widget):
        if self.border is not None:
            bounds = self.shape()
            p.setPen(self.border)
            #p.fillRect(bounds, QtGui.QColor(0, 0, 0))
            p.drawPath(bounds)

    #def saveSvg(self):
    #pass

    #def saveImage(self):
    #pass

    #def savePrint(self):
    #printer = QtGui.QPrinter()
    #if QtGui.QPrintDialog(printer).exec_() == QtGui.QDialog.Accepted:
    #p = QtGui.QPainter(printer)
    #p.setRenderHint(p.Antialiasing)
    #self.scene().render(p)
    #p.end()

    def updateViewLists(self):
        def cmpViews(a, b):
            wins = 100 * cmp(a.window() is self.window(),
                             b.window() is self.window())
            alpha = cmp(a.name, b.name)
            return wins + alpha

        ## make a sorted list of all named views
        nv = ViewBox.NamedViews.values()
        nv.sort(cmpViews)

        if self in nv:
            nv.remove(self)
        names = [v.name for v in nv]
        self.menu.setViewList(names)

    @staticmethod
    def updateAllViewLists():
        for v in ViewBox.AllViews:
            v.updateViewLists()