Merge tag 'pyqtgraph-0.9.10' into core

Conflicts:
	graphicsItems/ViewBox/ViewBox.py
	parametertree/SystemSolver.py
	widgets/SpinBox.py
This commit is contained in:
Luke Campagnola 2015-05-13 13:21:38 -04:00
commit 2e37d9b12c
29 changed files with 412 additions and 4471 deletions

View File

@ -84,8 +84,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj
def __init__(self, clickRadius=2, moveDistance=5):
QtGui.QGraphicsScene.__init__(self)
def __init__(self, clickRadius=2, moveDistance=5, parent=None):
QtGui.QGraphicsScene.__init__(self, parent)
self.setClickRadius(clickRadius)
self.setMoveDistance(moveDistance)
self.exportDirectory = None
@ -135,8 +135,13 @@ class GraphicsScene(QtGui.QGraphicsScene):
def mousePressEvent(self, ev):
#print 'scenePress'
QtGui.QGraphicsScene.mousePressEvent(self, ev)
#print "mouseGrabberItem: ", self.mouseGrabberItem()
if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events
if self.lastHoverEvent is not None:
# If the mouse has moved since the last hover event, send a new one.
# This can happen if a context menu is open while the mouse is moving.
if ev.scenePos() != self.lastHoverEvent.scenePos():
self.sendHoverEvents(ev)
self.clickEvents.append(MouseClickEvent(ev))
## set focus on the topmost focusable item under this click
@ -145,10 +150,6 @@ class GraphicsScene(QtGui.QGraphicsScene):
if i.isEnabled() and i.isVisible() and int(i.flags() & i.ItemIsFocusable) > 0:
i.setFocus(QtCore.Qt.MouseFocusReason)
break
#else:
#addr = sip.unwrapinstance(sip.cast(self.mouseGrabberItem(), QtGui.QGraphicsItem))
#item = GraphicsScene._addressCache.get(addr, self.mouseGrabberItem())
#print "click grabbed by:", item
def mouseMoveEvent(self, ev):
self.sigMouseMoved.emit(ev.scenePos())
@ -189,7 +190,6 @@ class GraphicsScene(QtGui.QGraphicsScene):
def mouseReleaseEvent(self, ev):
#print 'sceneRelease'
if self.mouseGrabberItem() is None:
#print "sending click/drag event"
if ev.button() in self.dragButtons:
if self.sendDragEvent(ev, final=True):
#print "sent drag event"
@ -231,6 +231,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
prevItems = list(self.hoverItems.keys())
#print "hover prev items:", prevItems
#print "hover test items:", items
for item in items:
if hasattr(item, 'hoverEvent'):
event.currentItem = item
@ -248,6 +250,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
event.enter = False
event.exit = True
#print "hover exit items:", prevItems
for item in prevItems:
event.currentItem = item
try:
@ -257,9 +260,13 @@ class GraphicsScene(QtGui.QGraphicsScene):
finally:
del self.hoverItems[item]
if hasattr(ev, 'buttons') and int(ev.buttons()) == 0:
# Update last hover event unless:
# - mouse is dragging (move+buttons); in this case we want the dragged
# item to continue receiving events until the drag is over
# - event is not a mouse event (QEvent.Leave sometimes appears here)
if (ev.type() == ev.GraphicsSceneMousePress or
(ev.type() == ev.GraphicsSceneMouseMove and int(ev.buttons()) == 0)):
self.lastHoverEvent = event ## save this so we can ask about accepted events later.
def sendDragEvent(self, ev, init=False, final=False):
## Send a MouseDragEvent to the current dragItem or to
@ -323,7 +330,6 @@ class GraphicsScene(QtGui.QGraphicsScene):
acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None)
else:
acceptedItem = None
if acceptedItem is not None:
ev.currentItem = acceptedItem
try:
@ -345,22 +351,9 @@ class GraphicsScene(QtGui.QGraphicsScene):
if int(item.flags() & item.ItemIsFocusable) > 0:
item.setFocus(QtCore.Qt.MouseFocusReason)
break
#if not ev.isAccepted() and ev.button() is QtCore.Qt.RightButton:
#print "GraphicsScene emitting sigSceneContextMenu"
#self.sigMouseClicked.emit(ev)
#ev.accept()
self.sigMouseClicked.emit(ev)
return ev.isAccepted()
#def claimEvent(self, item, button, eventType):
#key = (button, eventType)
#if key in self.claimedEvents:
#return False
#self.claimedEvents[key] = item
#print "event", key, "claimed by", item
#return True
def items(self, *args):
#print 'args:', args
items = QtGui.QGraphicsScene.items(self, *args)

View File

@ -355,6 +355,9 @@ class HoverEvent(object):
return Point(self.currentItem.mapFromScene(self._lastScenePos))
def __repr__(self):
if self.exit:
return "<HoverEvent exit=True>"
if self.currentItem is None:
lp = self._lastScenePos
p = self._scenePos

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
The file Image.py is a drop-in replacement for the same file in PIL 1.1.6.
It adds support for reading 16-bit TIFF files and converting then to numpy arrays.
(I submitted the changes to the PIL folks long ago, but to my knowledge the code
is not being used by them.)
To use, copy this file into
/usr/lib/python2.6/dist-packages/PIL/
or
C:\Python26\lib\site-packages\PIL\
..or wherever your system keeps its python modules.

View File

@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python
www.pyqtgraph.org
"""
__version__ = '0.9.8'
__version__ = '0.9.10'
### import all the goodies and add some helper functions for easy CLI use
@ -270,7 +270,12 @@ from .Qt import isQObjectAlive
## Attempts to work around exit crashes:
import atexit
_cleanupCalled = False
def cleanup():
global _cleanupCalled
if _cleanupCalled:
return
if not getConfigOption('exitCleanup'):
return
@ -295,8 +300,22 @@ def cleanup():
s.addItem(o)
except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object
continue
_cleanupCalled = True
atexit.register(cleanup)
# Call cleanup when QApplication quits. This is necessary because sometimes
# the QApplication will quit before the atexit callbacks are invoked.
# Note: cannot connect this function until QApplication has been created, so
# instead we have GraphicsView.__init__ call this for us.
_cleanupConnected = False
def _connectCleanup():
global _cleanupConnected
if _cleanupConnected:
return
QtGui.QApplication.instance().aboutToQuit.connect(cleanup)
_cleanupConnected = True
## Optional function for exiting immediately (with some manual teardown)
def exit():

View File

@ -29,7 +29,7 @@ def test_CSVExporter():
r = csv.reader(open('test.csv', 'r'))
lines = [line for line in r]
header = lines.pop(0)
assert header == ['myPlot_x', 'myPlot_y', 'x', 'y', 'x', 'y']
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
i = 0
for vals in lines:

View File

@ -823,16 +823,20 @@ class FlowchartWidget(dockarea.DockArea):
self.buildMenu()
def buildMenu(self, pos=None):
def buildSubMenu(node, rootMenu, subMenus, pos=None):
for section, node in node.items():
menu = QtGui.QMenu(section)
rootMenu.addMenu(menu)
if isinstance(node, OrderedDict):
buildSubMenu(node, menu, subMenus, pos=pos)
subMenus.append(menu)
else:
act = rootMenu.addAction(section)
act.nodeType = section
act.pos = pos
self.nodeMenu = QtGui.QMenu()
self.subMenus = []
for section, nodes in self.chart.library.getNodeTree().items():
menu = QtGui.QMenu(section)
self.nodeMenu.addMenu(menu)
for name in nodes:
act = menu.addAction(name)
act.nodeType = name
act.pos = pos
self.subMenus.append(menu)
self.subMenus = []
buildSubMenu(self.chart.library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos)
self.nodeMenu.triggered.connect(self.nodeMenuTriggered)
return self.nodeMenu

View File

@ -1222,6 +1222,8 @@ def downsample(data, n, axis=0, xvals='subsample'):
data = downsample(data, n[i], axis[i])
return data
if n <= 1:
return data
nPts = int(data.shape[axis] / n)
s = list(data.shape)
s[axis] = nPts

View File

@ -62,6 +62,11 @@ class AxisItem(GraphicsWidget):
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
self.textHeight = 18
# If the user specifies a width / height, remember that setting
# indefinitely.
self.fixedWidth = None
self.fixedHeight = None
self.labelText = ''
self.labelUnits = ''
self.labelUnitPrefix=''
@ -219,9 +224,9 @@ class AxisItem(GraphicsWidget):
#self.drawLabel = show
self.label.setVisible(show)
if self.orientation in ['left', 'right']:
self.setWidth()
self._updateWidth()
else:
self.setHeight()
self._updateHeight()
if self.autoSIPrefix:
self.updateAutoSIPrefix()
@ -291,54 +296,80 @@ class AxisItem(GraphicsWidget):
if mx > self.textWidth or mx < self.textWidth-10:
self.textWidth = mx
if self.style['autoExpandTextSpace'] is True:
self.setWidth()
self._updateWidth()
#return True ## size has changed
else:
mx = max(self.textHeight, x)
if mx > self.textHeight or mx < self.textHeight-10:
self.textHeight = mx
if self.style['autoExpandTextSpace'] is True:
self.setHeight()
self._updateHeight()
#return True ## size has changed
def _adjustSize(self):
if self.orientation in ['left', 'right']:
self.setWidth()
self._updateWidth()
else:
self.setHeight()
self._updateHeight()
def setHeight(self, h=None):
"""Set the height of this axis reserved for ticks and tick labels.
The height of the axis label is automatically added."""
if h is None:
if not self.style['showValues']:
h = 0
elif self.style['autoExpandTextSpace'] is True:
h = self.textHeight
The height of the axis label is automatically added.
If *height* is None, then the value will be determined automatically
based on the size of the tick text."""
self.fixedHeight = h
self._updateHeight()
def _updateHeight(self):
if not self.isVisible():
h = 0
else:
if self.fixedHeight is None:
if not self.style['showValues']:
h = 0
elif self.style['autoExpandTextSpace'] is True:
h = self.textHeight
else:
h = self.style['tickTextHeight']
h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0
h += max(0, self.style['tickLength'])
if self.label.isVisible():
h += self.label.boundingRect().height() * 0.8
else:
h = self.style['tickTextHeight']
h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0
h += max(0, self.style['tickLength'])
if self.label.isVisible():
h += self.label.boundingRect().height() * 0.8
h = self.fixedHeight
self.setMaximumHeight(h)
self.setMinimumHeight(h)
self.picture = None
def setWidth(self, w=None):
"""Set the width of this axis reserved for ticks and tick labels.
The width of the axis label is automatically added."""
if w is None:
if not self.style['showValues']:
w = 0
elif self.style['autoExpandTextSpace'] is True:
w = self.textWidth
The width of the axis label is automatically added.
If *width* is None, then the value will be determined automatically
based on the size of the tick text."""
self.fixedWidth = w
self._updateWidth()
def _updateWidth(self):
if not self.isVisible():
w = 0
else:
if self.fixedWidth is None:
if not self.style['showValues']:
w = 0
elif self.style['autoExpandTextSpace'] is True:
w = self.textWidth
else:
w = self.style['tickTextWidth']
w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0
w += max(0, self.style['tickLength'])
if self.label.isVisible():
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
else:
w = self.style['tickTextWidth']
w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0
w += max(0, self.style['tickLength'])
if self.label.isVisible():
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
w = self.fixedWidth
self.setMaximumWidth(w)
self.setMinimumWidth(w)
self.picture = None
@ -1009,19 +1040,18 @@ class AxisItem(GraphicsWidget):
profiler('draw text')
def show(self):
if self.orientation in ['left', 'right']:
self.setWidth()
else:
self.setHeight()
GraphicsWidget.show(self)
if self.orientation in ['left', 'right']:
self._updateWidth()
else:
self._updateHeight()
def hide(self):
if self.orientation in ['left', 'right']:
self.setWidth(0)
else:
self.setHeight(0)
GraphicsWidget.hide(self)
if self.orientation in ['left', 'right']:
self._updateWidth()
else:
self._updateHeight()
def wheelEvent(self, ev):
if self.linkedView() is None:

View File

@ -160,4 +160,12 @@ class GraphicsLayout(GraphicsWidget):
for i in list(self.items.keys()):
self.removeItem(i)
def setContentsMargins(self, *args):
# Wrap calls to layout. This should happen automatically, but there
# seems to be a Qt bug:
# http://stackoverflow.com/questions/27092164/margins-in-pyqtgraphs-graphicslayout
self.layout.setContentsMargins(*args)
def setSpacing(self, *args):
self.layout.setSpacing(*args)

View File

@ -49,7 +49,7 @@ class HistogramLUTItem(GraphicsWidget):
self.setLayout(self.layout)
self.layout.setContentsMargins(1,1,1,1)
self.layout.setSpacing(0)
self.vb = ViewBox()
self.vb = ViewBox(parent=self)
self.vb.setMaximumWidth(152)
self.vb.setMinimumWidth(45)
self.vb.setMouseEnabled(x=False, y=True)
@ -59,7 +59,7 @@ class HistogramLUTItem(GraphicsWidget):
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
self.region.setZValue(1000)
self.vb.addItem(self.region)
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10)
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self)
self.layout.addItem(self.axis, 0, 0)
self.layout.addItem(self.vb, 0, 1)
self.layout.addItem(self.gradient, 0, 2)

View File

@ -9,6 +9,8 @@ from .GraphicsObject import GraphicsObject
from ..Point import Point
__all__ = ['ImageItem']
class ImageItem(GraphicsObject):
"""
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`

View File

@ -145,7 +145,7 @@ class PlotItem(GraphicsWidget):
self.layout.setVerticalSpacing(0)
if viewBox is None:
viewBox = ViewBox()
viewBox = ViewBox(parent=self)
self.vb = viewBox
self.vb.sigStateChanged.connect(self.viewStateChanged)
self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus
@ -168,14 +168,14 @@ class PlotItem(GraphicsWidget):
axisItems = {}
self.axes = {}
for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
axis = axisItems.get(k, AxisItem(orientation=k))
axis = axisItems.get(k, AxisItem(orientation=k, parent=self))
axis.linkToView(self.vb)
self.axes[k] = {'item': axis, 'pos': pos}
self.layout.addItem(axis, *pos)
axis.setZValue(-1000)
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
self.titleLabel = LabelItem('', size='11pt')
self.titleLabel = LabelItem('', size='11pt', parent=self)
self.layout.addItem(self.titleLabel, 0, 1)
self.setTitle(None) ## hide

View File

@ -241,8 +241,8 @@ class ScatterPlotItem(GraphicsObject):
'useCache': True, ## If useCache is False, symbols are re-drawn on every paint.
'antialias': getConfigOption('antialias'),
'name': None,
}
}
self.setPen(fn.mkPen(getConfigOption('foreground')), update=False)
self.setBrush(fn.mkBrush(100,100,150), update=False)
self.setSymbol('o', update=False)
@ -351,16 +351,12 @@ class ScatterPlotItem(GraphicsObject):
newData = self.data[len(oldData):]
newData['size'] = -1 ## indicates to use default size
if 'spots' in kargs:
spots = kargs['spots']
for i in range(len(spots)):
spot = spots[i]
for k in spot:
#if k == 'pen':
#newData[k] = fn.mkPen(spot[k])
#elif k == 'brush':
#newData[k] = fn.mkBrush(spot[k])
if k == 'pos':
pos = spot[k]
if isinstance(pos, QtCore.QPointF):
@ -369,10 +365,12 @@ class ScatterPlotItem(GraphicsObject):
x,y = pos[0], pos[1]
newData[i]['x'] = x
newData[i]['y'] = y
elif k in ['x', 'y', 'size', 'symbol', 'pen', 'brush', 'data']:
elif k == 'pen':
newData[i][k] = fn.mkPen(spot[k])
elif k == 'brush':
newData[i][k] = fn.mkBrush(spot[k])
elif k in ['x', 'y', 'size', 'symbol', 'brush', 'data']:
newData[i][k] = spot[k]
#elif k == 'data':
#self.pointData[i] = spot[k]
else:
raise Exception("Unknown spot parameter: %s" % k)
elif 'y' in kargs:
@ -389,10 +387,10 @@ class ScatterPlotItem(GraphicsObject):
if k in kargs:
setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
setMethod(kargs[k], update=False, dataSet=newData, mask=kargs.get('mask', None))
if 'data' in kargs:
self.setPointData(kargs['data'], dataSet=newData)
self.prepareGeometryChange()
self.informViewBoundsChanged()
self.bounds = [None, None]
@ -428,10 +426,10 @@ class ScatterPlotItem(GraphicsObject):
all spots which do not have a pen explicitly set."""
update = kargs.pop('update', True)
dataSet = kargs.pop('dataSet', self.data)
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
pens = args[0]
if kargs['mask'] is not None:
if 'mask' in kargs and kargs['mask'] is not None:
pens = pens[kargs['mask']]
if len(pens) != len(dataSet):
raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet)))
@ -453,7 +451,7 @@ class ScatterPlotItem(GraphicsObject):
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
brushes = args[0]
if kargs['mask'] is not None:
if 'mask' in kargs and kargs['mask'] is not None:
brushes = brushes[kargs['mask']]
if len(brushes) != len(dataSet):
raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet)))

View File

@ -44,6 +44,11 @@ class TextItem(UIGraphicsItem):
self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
def setText(self, text, color=(200,200,200)):
"""
Set the text and color of this item.
This method sets the plain text of the item; see also setHtml().
"""
color = fn.mkColor(color)
self.textItem.setDefaultTextColor(color)
self.textItem.setPlainText(text)
@ -57,18 +62,41 @@ class TextItem(UIGraphicsItem):
#self.translate(0, 20)
def setPlainText(self, *args):
"""
Set the plain text to be rendered by this item.
See QtGui.QGraphicsTextItem.setPlainText().
"""
self.textItem.setPlainText(*args)
self.updateText()
def setHtml(self, *args):
"""
Set the HTML code to be rendered by this item.
See QtGui.QGraphicsTextItem.setHtml().
"""
self.textItem.setHtml(*args)
self.updateText()
def setTextWidth(self, *args):
"""
Set the width of the text.
If the text requires more space than the width limit, then it will be
wrapped into multiple lines.
See QtGui.QGraphicsTextItem.setTextWidth().
"""
self.textItem.setTextWidth(*args)
self.updateText()
def setFont(self, *args):
"""
Set the font for this text.
See QtGui.QGraphicsTextItem.setFont().
"""
self.textItem.setFont(*args)
self.updateText()

View File

@ -427,11 +427,11 @@ class ViewBox(GraphicsWidget):
self.linkedYChanged()
self.updateAutoRange()
self.updateViewRange()
self._matrixNeedsUpdate = True
self.sigStateChanged.emit(self)
self.background.setRect(self.rect())
self.sigResized.emit(self)
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
@ -909,7 +909,7 @@ class ViewBox(GraphicsWidget):
if k in args:
if not np.all(np.isfinite(args[k])):
r = args.pop(k)
print "Warning: %s is invalid: %s" % (k, str(r))
#print("Warning: %s is invalid: %s" % (k, str(r))
self.setRange(**args)
finally:
@ -1135,6 +1135,8 @@ class ViewBox(GraphicsWidget):
Return the transform that maps from child(item in the childGroup) coordinates to local coordinates.
(This maps from inside the viewbox to outside)
"""
if self._matrixNeedsUpdate:
self.updateMatrix()
m = self.childGroup.transform()
#m1 = QtGui.QTransform()
#m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y())
@ -1694,6 +1696,8 @@ class ViewBox(GraphicsWidget):
def forgetView(vid, name):
if ViewBox is None: ## can happen as python is shutting down
return
if QtGui.QApplication.instance() is None:
return
## Called with ID and name of view (the view itself is no longer available)
for v in list(ViewBox.AllViews.keys()):
if id(v) == vid:
@ -1716,6 +1720,8 @@ class ViewBox(GraphicsWidget):
pass
except TypeError: ## view has already been deleted (?)
pass
except AttributeError: # PySide has deleted signal
pass
def locate(self, item, timeout=3.0, children=False):
"""

View File

@ -20,11 +20,11 @@ def test_ViewBox():
win.show()
vb = win.addViewBox()
# set range before viewbox is shown
vb.setRange(xRange=[0, 10], yRange=[0, 10], padding=0)
# required to make mapFromView work properly.
qtest.qWaitForWindowShown(win)
vb.update()
g = pg.GridItem()
vb.addItem(g)

View File

@ -1,23 +0,0 @@
import pyqtgraph as pg
import numpy as np
app = pg.mkQApp()
plot = pg.plot()
app.processEvents()
# set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect())
def test_modes():
for i, pxMode in enumerate([True, False]):
for j, useCache in enumerate([True, False]):
s = pg.ScatterPlotItem()
s.opts['useCache'] = useCache
plot.addItem(s)
s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode)
s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30])
if __name__ == '__main__':
test_modes()

View File

@ -1,95 +0,0 @@
"""
ViewBox test cases:
* call setRange then resize; requested range must be fully visible
* lockAspect works correctly for arbitrary aspect ratio
* autoRange works correctly with aspect locked
* call setRange with aspect locked, then resize
* AutoRange with all the bells and whistles
* item moves / changes transformation / changes bounds
* pan only
* fractional range
"""
import pyqtgraph as pg
app = pg.mkQApp()
imgData = pg.np.zeros((10, 10))
imgData[0] = 3
imgData[-1] = 3
imgData[:,0] = 3
imgData[:,-1] = 3
def testLinkWithAspectLock():
global win, vb
win = pg.GraphicsWindow()
vb = win.addViewBox(name="image view")
vb.setAspectLocked()
vb.enableAutoRange(x=False, y=False)
p1 = win.addPlot(name="plot 1")
p2 = win.addPlot(name="plot 2", row=1, col=0)
win.ci.layout.setRowFixedHeight(1, 150)
win.ci.layout.setColumnFixedWidth(1, 150)
def viewsMatch():
r0 = pg.np.array(vb.viewRange())
r1 = pg.np.array(p1.vb.viewRange()[1])
r2 = pg.np.array(p2.vb.viewRange()[1])
match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all()
return match
p1.setYLink(vb)
p2.setXLink(vb)
print "link views match:", viewsMatch()
win.show()
print "show views match:", viewsMatch()
img = pg.ImageItem(imgData)
vb.addItem(img)
vb.autoRange()
p1.plot(x=imgData.sum(axis=0), y=range(10))
p2.plot(x=range(10), y=imgData.sum(axis=1))
print "add items views match:", viewsMatch()
#p1.setAspectLocked()
#grid = pg.GridItem()
#vb.addItem(grid)
pg.QtGui.QApplication.processEvents()
pg.QtGui.QApplication.processEvents()
#win.resize(801, 600)
def testAspectLock():
global win, vb
win = pg.GraphicsWindow()
vb = win.addViewBox(name="image view")
vb.setAspectLocked()
img = pg.ImageItem(imgData)
vb.addItem(img)
#app.processEvents()
#print "init views match:", viewsMatch()
#p2.setYRange(-300, 300)
#print "setRange views match:", viewsMatch()
#app.processEvents()
#print "setRange views match (after update):", viewsMatch()
#print "--lock aspect--"
#p1.setAspectLocked(True)
#print "lockAspect views match:", viewsMatch()
#p2.setYRange(-200, 200)
#print "setRange views match:", viewsMatch()
#app.processEvents()
#print "setRange views match (after update):", viewsMatch()
#win.resize(100, 600)
#app.processEvents()
#vb.setRange(xRange=[-10, 10], padding=0)
#app.processEvents()
#win.resize(600, 100)
#app.processEvents()
#print vb.viewRange()
if __name__ == '__main__':
testLinkWithAspectLock()

View File

@ -0,0 +1,86 @@
import pyqtgraph as pg
import numpy as np
app = pg.mkQApp()
plot = pg.plot()
app.processEvents()
# set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect())
def test_scatterplotitem():
for i, pxMode in enumerate([True, False]):
for j, useCache in enumerate([True, False]):
s = pg.ScatterPlotItem()
s.opts['useCache'] = useCache
plot.addItem(s)
s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode)
s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30])
# Test uniform spot updates
s.setSize(10)
s.setBrush('r')
s.setPen('g')
s.setSymbol('+')
app.processEvents()
# Test list spot updates
s.setSize([10] * 6)
s.setBrush([pg.mkBrush('r')] * 6)
s.setPen([pg.mkPen('g')] * 6)
s.setSymbol(['+'] * 6)
s.setPointData([s] * 6)
app.processEvents()
# Test array spot updates
s.setSize(np.array([10] * 6))
s.setBrush(np.array([pg.mkBrush('r')] * 6))
s.setPen(np.array([pg.mkPen('g')] * 6))
s.setSymbol(np.array(['+'] * 6))
s.setPointData(np.array([s] * 6))
app.processEvents()
# Test per-spot updates
spot = s.points()[0]
spot.setSize(20)
spot.setBrush('b')
spot.setPen('g')
spot.setSymbol('o')
spot.setData(None)
app.processEvents()
plot.clear()
def test_init_spots():
spots = [
{'x': 0, 'y': 1},
{'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'},
]
s = pg.ScatterPlotItem(spots=spots)
# Check we can display without errors
plot.addItem(s)
app.processEvents()
plot.clear()
# check data is correct
spots = s.points()
defPen = pg.mkPen(pg.getConfigOption('foreground'))
assert spots[0].pos().x() == 0
assert spots[0].pos().y() == 1
assert spots[0].pen() == defPen
assert spots[0].data() is None
assert spots[1].pos().x() == 1
assert spots[1].pos().y() == 2
assert spots[1].pen() == pg.mkPen(None)
assert spots[1].brush() == pg.mkBrush(None)
assert spots[1].data() == 'zzz'
if __name__ == '__main__':
test_scatterplotitem()

View File

@ -28,8 +28,13 @@ GLOptions = {
class GLGraphicsItem(QtCore.QObject):
_nextId = 0
def __init__(self, parentItem=None):
QtCore.QObject.__init__(self)
self._id = GLGraphicsItem._nextId
GLGraphicsItem._nextId += 1
self.__parent = None
self.__view = None
self.__children = set()

View File

@ -7,6 +7,8 @@ from .. import functions as fn
##Vector = QtGui.QVector3D
ShareWidget = None
class GLViewWidget(QtOpenGL.QGLWidget):
"""
Basic widget for displaying 3D data
@ -16,14 +18,14 @@ class GLViewWidget(QtOpenGL.QGLWidget):
"""
ShareWidget = None
def __init__(self, parent=None):
if GLViewWidget.ShareWidget is None:
global ShareWidget
if ShareWidget is None:
## create a dummy widget to allow sharing objects (textures, shaders, etc) between views
GLViewWidget.ShareWidget = QtOpenGL.QGLWidget()
ShareWidget = QtOpenGL.QGLWidget()
QtOpenGL.QGLWidget.__init__(self, parent, GLViewWidget.ShareWidget)
QtOpenGL.QGLWidget.__init__(self, parent, ShareWidget)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
@ -157,7 +159,6 @@ class GLViewWidget(QtOpenGL.QGLWidget):
items = [(h.near, h.names[0]) for h in hits]
items.sort(key=lambda i: i[0])
return [self._itemNames[i[1]] for i in items]
def paintGL(self, region=None, viewport=None, useItemNames=False):
@ -191,8 +192,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
try:
glPushAttrib(GL_ALL_ATTRIB_BITS)
if useItemNames:
glLoadName(id(i))
self._itemNames[id(i)] = i
glLoadName(i._id)
self._itemNames[i._id] = i
i.paint()
except:
from .. import debug

View File

@ -66,7 +66,8 @@ class GLScatterPlotItem(GLGraphicsItem):
#print pData.shape, pData.min(), pData.max()
pData = pData.astype(np.ubyte)
self.pointTexture = glGenTextures(1)
if getattr(self, "pointTexture", None) is None:
self.pointTexture = glGenTextures(1)
glActiveTexture(GL_TEXTURE0)
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.pointTexture)

View File

@ -377,5 +377,5 @@ if __name__ == '__main__':
camera.flash = 0
camera.solve()
print camera.saveState()
print(camera.saveState())

38
tests/test_exit_crash.py Normal file
View File

@ -0,0 +1,38 @@
import os, sys, subprocess, tempfile
import pyqtgraph as pg
code = """
import sys
sys.path.insert(0, '{path}')
import pyqtgraph as pg
app = pg.mkQApp()
w = pg.{classname}({args})
"""
def test_exit_crash():
# For each Widget subclass, run a simple python script that creates an
# instance and then shuts down. The intent is to check for segmentation
# faults when each script exits.
tmp = tempfile.mktemp(".py")
path = os.path.dirname(pg.__file__)
initArgs = {
'CheckTable': "[]",
'ProgressDialog': '"msg"',
'VerticalLabel': '"msg"',
}
for name in dir(pg):
obj = getattr(pg, name)
if not isinstance(obj, type) or not issubclass(obj, pg.QtGui.QWidget):
continue
print name
argstr = initArgs.get(name, "")
open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr))
proc = subprocess.Popen([sys.executable, tmp])
assert proc.wait() == 0
os.remove(tmp)

64
util/pil_fix.py Normal file
View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
Importing this module installs support for 16-bit images in PIL.
This works by patching objects in the PIL namespace; no files are
modified.
"""
from PIL import Image
if Image.VERSION == '1.1.7':
Image._MODE_CONV["I;16"] = ('%su2' % Image._ENDIAN, None)
Image._fromarray_typemap[((1, 1), "<u2")] = ("I", "I;16")
if Image.VERSION == '1.1.6':
Image._MODE_CONV["I;16"] = ('%su2' % Image._ENDIAN, None)
## just a copy of fromarray() from Image.py with I;16 added in
def fromarray(obj, mode=None):
arr = obj.__array_interface__
shape = arr['shape']
ndim = len(shape)
try:
strides = arr['strides']
except KeyError:
strides = None
if mode is None:
typestr = arr['typestr']
if not (typestr[0] == '|' or typestr[0] == Image._ENDIAN or
typestr[1:] not in ['u1', 'b1', 'i4', 'f4']):
raise TypeError("cannot handle data-type")
if typestr[0] == Image._ENDIAN:
typestr = typestr[1:3]
else:
typestr = typestr[:2]
if typestr == 'i4':
mode = 'I'
if typestr == 'u2':
mode = 'I;16'
elif typestr == 'f4':
mode = 'F'
elif typestr == 'b1':
mode = '1'
elif ndim == 2:
mode = 'L'
elif ndim == 3:
mode = 'RGB'
elif ndim == 4:
mode = 'RGBA'
else:
raise TypeError("Do not understand data.")
ndmax = 4
bad_dims=0
if mode in ['1','L','I','P','F']:
ndmax = 2
elif mode == 'RGB':
ndmax = 3
if ndim > ndmax:
raise ValueError("Too many dimensions.")
size = shape[:2][::-1]
if strides is not None:
obj = obj.tostring()
return frombuffer(mode, size, obj, "raw", mode, 0, 1)
Image.fromarray=fromarray

View File

@ -71,6 +71,13 @@ class GraphicsView(QtGui.QGraphicsView):
QtGui.QGraphicsView.__init__(self, parent)
# This connects a cleanup function to QApplication.aboutToQuit. It is
# called from here because we have no good way to react when the
# QApplication is created by the user.
# See pyqtgraph.__init__.py
from .. import _connectCleanup
_connectCleanup()
if useOpenGL is None:
useOpenGL = getConfigOption('useOpenGL')
@ -102,7 +109,8 @@ class GraphicsView(QtGui.QGraphicsView):
self.currentItem = None
self.clearMouse()
self.updateMatrix()
self.sceneObj = GraphicsScene()
# GraphicsScene must have parent or expect crashes!
self.sceneObj = GraphicsScene(parent=self)
self.setScene(self.sceneObj)
## Workaround for PySide crash
@ -143,7 +151,6 @@ class GraphicsView(QtGui.QGraphicsView):
def paintEvent(self, ev):
self.scene().prepareForPaint()
#print "GV: paint", ev.rect()
return QtGui.QGraphicsView.paintEvent(self, ev)
def render(self, *args, **kwds):

View File

@ -239,12 +239,15 @@ class SpinBox(QtGui.QAbstractSpinBox):
Select the numerical portion of the text to allow quick editing by the user.
"""
le = self.lineEdit()
text = le.text()
try:
index = text.index(' ')
except ValueError:
return
le.setSelection(0, index)
text = asUnicode(le.text())
if self.opts['suffix'] == '':
le.setSelection(0, len(text))
else:
try:
index = text.index(' ')
except ValueError:
return
le.setSelection(0, index)
def value(self):
"""