Merge branch 'develop' into pyqtgraph-core
This commit is contained in:
commit
f613d33c49
@ -48,8 +48,8 @@ else:
|
||||
CONFIG_OPTIONS = {
|
||||
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
|
||||
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
|
||||
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
|
||||
'background': (0, 0, 0), ## default background for GraphicsWidget
|
||||
'foreground': 'd', ## default foreground color for axes, labels, etc.
|
||||
'background': 'k', ## default background for GraphicsWidget
|
||||
'antialias': False,
|
||||
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
||||
'useWeave': True, ## Use weave to speed up some operations, if it is available
|
||||
|
@ -58,14 +58,15 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
|
||||
|
||||
|
||||
class Flowchart(Node):
|
||||
|
||||
sigFileLoaded = QtCore.Signal(object)
|
||||
sigFileSaved = QtCore.Signal(object)
|
||||
|
||||
|
||||
#sigOutputChanged = QtCore.Signal() ## inherited from Node
|
||||
sigChartLoaded = QtCore.Signal()
|
||||
sigStateChanged = QtCore.Signal()
|
||||
sigStateChanged = QtCore.Signal() # called when output is expected to have changed
|
||||
sigChartChanged = QtCore.Signal(object, object, object) # called when nodes are added, removed, or renamed.
|
||||
# (self, action, node)
|
||||
|
||||
def __init__(self, terminals=None, name=None, filePath=None, library=None):
|
||||
self.library = library or LIBRARY
|
||||
@ -218,6 +219,7 @@ class Flowchart(Node):
|
||||
node.sigClosed.connect(self.nodeClosed)
|
||||
node.sigRenamed.connect(self.nodeRenamed)
|
||||
node.sigOutputChanged.connect(self.nodeOutputChanged)
|
||||
self.sigChartChanged.emit(self, 'add', node)
|
||||
|
||||
def removeNode(self, node):
|
||||
node.close()
|
||||
@ -237,11 +239,13 @@ class Flowchart(Node):
|
||||
node.sigOutputChanged.disconnect(self.nodeOutputChanged)
|
||||
except TypeError:
|
||||
pass
|
||||
self.sigChartChanged.emit(self, 'remove', node)
|
||||
|
||||
def nodeRenamed(self, node, oldName):
|
||||
del self._nodes[oldName]
|
||||
self._nodes[node.name()] = node
|
||||
self.widget().nodeRenamed(node, oldName)
|
||||
self.sigChartChanged.emit(self, 'rename', node)
|
||||
|
||||
def arrangeNodes(self):
|
||||
pass
|
||||
|
@ -4,7 +4,7 @@ import weakref
|
||||
from ...Qt import QtCore, QtGui
|
||||
from ...graphicsItems.ScatterPlotItem import ScatterPlotItem
|
||||
from ...graphicsItems.PlotCurveItem import PlotCurveItem
|
||||
from ... import PlotDataItem
|
||||
from ... import PlotDataItem, ComboBox
|
||||
|
||||
from .common import *
|
||||
import numpy as np
|
||||
@ -16,7 +16,9 @@ class PlotWidgetNode(Node):
|
||||
|
||||
def __init__(self, name):
|
||||
Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}})
|
||||
self.plot = None
|
||||
self.plot = None # currently selected plot
|
||||
self.plots = {} # list of available plots user may select from
|
||||
self.ui = None
|
||||
self.items = {}
|
||||
|
||||
def disconnected(self, localTerm, remoteTerm):
|
||||
@ -26,16 +28,27 @@ class PlotWidgetNode(Node):
|
||||
|
||||
def setPlot(self, plot):
|
||||
#print "======set plot"
|
||||
if plot == self.plot:
|
||||
return
|
||||
|
||||
# clear data from previous plot
|
||||
if self.plot is not None:
|
||||
for vid in list(self.items.keys()):
|
||||
self.plot.removeItem(self.items[vid])
|
||||
del self.items[vid]
|
||||
|
||||
self.plot = plot
|
||||
self.updateUi()
|
||||
self.update()
|
||||
self.sigPlotChanged.emit(self)
|
||||
|
||||
def getPlot(self):
|
||||
return self.plot
|
||||
|
||||
def process(self, In, display=True):
|
||||
if display:
|
||||
#self.plot.clearPlots()
|
||||
if display and self.plot is not None:
|
||||
items = set()
|
||||
# Add all new input items to selected plot
|
||||
for name, vals in In.items():
|
||||
if vals is None:
|
||||
continue
|
||||
@ -45,14 +58,13 @@ class PlotWidgetNode(Node):
|
||||
for val in vals:
|
||||
vid = id(val)
|
||||
if vid in self.items and self.items[vid].scene() is self.plot.scene():
|
||||
# Item is already added to the correct scene
|
||||
# possible bug: what if two plots occupy the same scene? (should
|
||||
# rarely be a problem because items are removed from a plot before
|
||||
# switching).
|
||||
items.add(vid)
|
||||
else:
|
||||
#if isinstance(val, PlotCurveItem):
|
||||
#self.plot.addItem(val)
|
||||
#item = val
|
||||
#if isinstance(val, ScatterPlotItem):
|
||||
#self.plot.addItem(val)
|
||||
#item = val
|
||||
# Add the item to the plot, or generate a new item if needed.
|
||||
if isinstance(val, QtGui.QGraphicsItem):
|
||||
self.plot.addItem(val)
|
||||
item = val
|
||||
@ -60,22 +72,48 @@ class PlotWidgetNode(Node):
|
||||
item = self.plot.plot(val)
|
||||
self.items[vid] = item
|
||||
items.add(vid)
|
||||
|
||||
# Any left-over items that did not appear in the input must be removed
|
||||
for vid in list(self.items.keys()):
|
||||
if vid not in items:
|
||||
#print "remove", self.items[vid]
|
||||
self.plot.removeItem(self.items[vid])
|
||||
del self.items[vid]
|
||||
|
||||
def processBypassed(self, args):
|
||||
if self.plot is None:
|
||||
return
|
||||
for item in list(self.items.values()):
|
||||
self.plot.removeItem(item)
|
||||
self.items = {}
|
||||
|
||||
#def setInput(self, **args):
|
||||
#for k in args:
|
||||
#self.plot.plot(args[k])
|
||||
def ctrlWidget(self):
|
||||
if self.ui is None:
|
||||
self.ui = ComboBox()
|
||||
self.ui.currentIndexChanged.connect(self.plotSelected)
|
||||
self.updateUi()
|
||||
return self.ui
|
||||
|
||||
|
||||
def plotSelected(self, index):
|
||||
self.setPlot(self.ui.value())
|
||||
|
||||
def setPlotList(self, plots):
|
||||
"""
|
||||
Specify the set of plots (PlotWidget or PlotItem) that the user may
|
||||
select from.
|
||||
|
||||
*plots* must be a dictionary of {name: plot} pairs.
|
||||
"""
|
||||
self.plots = plots
|
||||
self.updateUi()
|
||||
|
||||
def updateUi(self):
|
||||
# sets list and automatically preserves previous selection
|
||||
self.ui.setItems(self.plots)
|
||||
try:
|
||||
self.ui.setValue(self.plot)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class CanvasNode(Node):
|
||||
"""Connection to a Canvas widget."""
|
||||
|
44
functions.py
44
functions.py
@ -7,15 +7,19 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
|
||||
from __future__ import division
|
||||
from .python2_3 import asUnicode
|
||||
from .Qt import QtGui, QtCore, USE_PYSIDE
|
||||
Colors = {
|
||||
'b': (0,0,255,255),
|
||||
'g': (0,255,0,255),
|
||||
'r': (255,0,0,255),
|
||||
'c': (0,255,255,255),
|
||||
'm': (255,0,255,255),
|
||||
'y': (255,255,0,255),
|
||||
'k': (0,0,0,255),
|
||||
'w': (255,255,255,255),
|
||||
'b': QtGui.QColor(0,0,255,255),
|
||||
'g': QtGui.QColor(0,255,0,255),
|
||||
'r': QtGui.QColor(255,0,0,255),
|
||||
'c': QtGui.QColor(0,255,255,255),
|
||||
'm': QtGui.QColor(255,0,255,255),
|
||||
'y': QtGui.QColor(255,255,0,255),
|
||||
'k': QtGui.QColor(0,0,0,255),
|
||||
'w': QtGui.QColor(255,255,255,255),
|
||||
'd': QtGui.QColor(150,150,150,255),
|
||||
'l': QtGui.QColor(200,200,200,255),
|
||||
's': QtGui.QColor(100,100,150,255),
|
||||
}
|
||||
|
||||
SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY')
|
||||
@ -168,17 +172,15 @@ def mkColor(*args):
|
||||
"""
|
||||
err = 'Not sure how to make a color from "%s"' % str(args)
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], QtGui.QColor):
|
||||
return QtGui.QColor(args[0])
|
||||
elif isinstance(args[0], float):
|
||||
r = g = b = int(args[0] * 255)
|
||||
a = 255
|
||||
elif isinstance(args[0], basestring):
|
||||
if isinstance(args[0], basestring):
|
||||
c = args[0]
|
||||
if c[0] == '#':
|
||||
c = c[1:]
|
||||
if len(c) == 1:
|
||||
(r, g, b, a) = Colors[c]
|
||||
try:
|
||||
return Colors[c]
|
||||
except KeyError:
|
||||
raise Exception('No color named "%s"' % c)
|
||||
if len(c) == 3:
|
||||
r = int(c[0]*2, 16)
|
||||
g = int(c[1]*2, 16)
|
||||
@ -199,6 +201,11 @@ def mkColor(*args):
|
||||
g = int(c[2:4], 16)
|
||||
b = int(c[4:6], 16)
|
||||
a = int(c[6:8], 16)
|
||||
elif isinstance(args[0], QtGui.QColor):
|
||||
return QtGui.QColor(args[0])
|
||||
elif isinstance(args[0], float):
|
||||
r = g = b = int(args[0] * 255)
|
||||
a = 255
|
||||
elif hasattr(args[0], '__len__'):
|
||||
if len(args[0]) == 3:
|
||||
(r, g, b) = args[0]
|
||||
@ -282,7 +289,7 @@ def mkPen(*args, **kargs):
|
||||
color = args
|
||||
|
||||
if color is None:
|
||||
color = mkColor(200, 200, 200)
|
||||
color = mkColor('l')
|
||||
if hsv is not None:
|
||||
color = hsvColor(*hsv)
|
||||
else:
|
||||
@ -821,7 +828,10 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
minVal, maxVal = levels
|
||||
if minVal == maxVal:
|
||||
maxVal += 1e-16
|
||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
|
||||
if maxVal == minVal:
|
||||
data = rescaleData(data, 1, minVal, dtype=int)
|
||||
else:
|
||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
|
||||
|
||||
profile()
|
||||
|
||||
|
@ -277,11 +277,11 @@ class AxisItem(GraphicsWidget):
|
||||
if pen == None, the default will be used (see :func:`setConfigOption
|
||||
<pyqtgraph.setConfigOption>`)
|
||||
"""
|
||||
self._pen = pen
|
||||
self.picture = None
|
||||
if pen is None:
|
||||
pen = getConfigOption('foreground')
|
||||
self.labelStyle['color'] = '#' + fn.colorStr(fn.mkPen(pen).color())[:6]
|
||||
self._pen = fn.mkPen(pen)
|
||||
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
||||
self.setLabel()
|
||||
self.update()
|
||||
|
||||
@ -458,8 +458,7 @@ class AxisItem(GraphicsWidget):
|
||||
return []
|
||||
|
||||
## decide optimal minor tick spacing in pixels (this is just aesthetics)
|
||||
pixelSpacing = size / np.log(size)
|
||||
optimalTickCount = max(2., size / pixelSpacing)
|
||||
optimalTickCount = max(2., np.log(size))
|
||||
|
||||
## optimal minor tick spacing
|
||||
optimalSpacing = dif / optimalTickCount
|
||||
@ -795,7 +794,7 @@ class AxisItem(GraphicsWidget):
|
||||
if s is None:
|
||||
rects.append(None)
|
||||
else:
|
||||
br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s))
|
||||
br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, asUnicode(s))
|
||||
## boundingRect is usually just a bit too large
|
||||
## (but this probably depends on per-font metrics?)
|
||||
br.setHeight(br.height() * 0.8)
|
||||
@ -830,7 +829,7 @@ class AxisItem(GraphicsWidget):
|
||||
vstr = strings[j]
|
||||
if vstr is None: ## this tick was ignored because it is out of bounds
|
||||
continue
|
||||
vstr = str(vstr)
|
||||
vstr = asUnicode(vstr)
|
||||
x = tickPositions[i][j]
|
||||
#textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
|
||||
textRect = rects[j]
|
||||
|
@ -1,24 +1,70 @@
|
||||
from ..Qt import QtGui
|
||||
from .. import functions as fn
|
||||
from .PlotDataItem import PlotDataItem
|
||||
from .PlotCurveItem import PlotCurveItem
|
||||
|
||||
class FillBetweenItem(QtGui.QGraphicsPathItem):
|
||||
"""
|
||||
GraphicsItem filling the space between two PlotDataItems.
|
||||
"""
|
||||
def __init__(self, p1, p2, brush=None):
|
||||
def __init__(self, curve1=None, curve2=None, brush=None):
|
||||
QtGui.QGraphicsPathItem.__init__(self)
|
||||
self.p1 = p1
|
||||
self.p2 = p2
|
||||
p1.sigPlotChanged.connect(self.updatePath)
|
||||
p2.sigPlotChanged.connect(self.updatePath)
|
||||
self.curves = None
|
||||
if curve1 is not None and curve2 is not None:
|
||||
self.setCurves(curve1, curve2)
|
||||
elif curve1 is not None or curve2 is not None:
|
||||
raise Exception("Must specify two curves to fill between.")
|
||||
|
||||
if brush is not None:
|
||||
self.setBrush(fn.mkBrush(brush))
|
||||
self.setZValue(min(p1.zValue(), p2.zValue())-1)
|
||||
self.updatePath()
|
||||
|
||||
def setCurves(self, curve1, curve2):
|
||||
"""Set the curves to fill between.
|
||||
|
||||
Arguments must be instances of PlotDataItem or PlotCurveItem."""
|
||||
|
||||
if self.curves is not None:
|
||||
for c in self.curves:
|
||||
try:
|
||||
c.sigPlotChanged.disconnect(self.curveChanged)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
curves = [curve1, curve2]
|
||||
for c in curves:
|
||||
if not isinstance(c, PlotDataItem) and not isinstance(c, PlotCurveItem):
|
||||
raise TypeError("Curves must be PlotDataItem or PlotCurveItem.")
|
||||
self.curves = curves
|
||||
curve1.sigPlotChanged.connect(self.curveChanged)
|
||||
curve2.sigPlotChanged.connect(self.curveChanged)
|
||||
self.setZValue(min(curve1.zValue(), curve2.zValue())-1)
|
||||
self.curveChanged()
|
||||
|
||||
def setBrush(self, *args, **kwds):
|
||||
"""Change the fill brush. Acceps the same arguments as pg.mkBrush()"""
|
||||
QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds))
|
||||
|
||||
def curveChanged(self):
|
||||
self.updatePath()
|
||||
|
||||
def updatePath(self):
|
||||
p1 = self.p1.curve.path
|
||||
p2 = self.p2.curve.path
|
||||
if self.curves is None:
|
||||
self.setPath(QtGui.QPainterPath())
|
||||
return
|
||||
paths = []
|
||||
for c in self.curves:
|
||||
if isinstance(c, PlotDataItem):
|
||||
paths.append(c.curve.getPath())
|
||||
elif isinstance(c, PlotCurveItem):
|
||||
paths.append(c.getPath())
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.addPolygon(p1.toSubpathPolygons()[0] + p2.toReversed().toSubpathPolygons()[0])
|
||||
p1 = paths[0].toSubpathPolygons()
|
||||
p2 = paths[1].toReversed().toSubpathPolygons()
|
||||
if len(p1) == 0 or len(p2) == 0:
|
||||
self.setPath(QtGui.QPainterPath())
|
||||
return
|
||||
|
||||
path.addPolygon(p1[0] + p2[0])
|
||||
self.setPath(path)
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import division
|
||||
|
||||
from ..Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
import collections
|
||||
@ -287,15 +289,45 @@ class ImageItem(GraphicsObject):
|
||||
self.render()
|
||||
self.qimage.save(fileName, *args)
|
||||
|
||||
def getHistogram(self, bins=500, step=3):
|
||||
def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds):
|
||||
"""Returns x and y arrays containing the histogram values for the current image.
|
||||
The step argument causes pixels to be skipped when computing the histogram to save time.
|
||||
For an explanation of the return format, see numpy.histogram().
|
||||
|
||||
The *step* argument causes pixels to be skipped when computing the histogram to save time.
|
||||
If *step* is 'auto', then a step is chosen such that the analyzed data has
|
||||
dimensions roughly *targetImageSize* for each axis.
|
||||
|
||||
The *bins* argument and any extra keyword arguments are passed to
|
||||
np.histogram(). If *bins* is 'auto', then a bin number is automatically
|
||||
chosen based on the image characteristics:
|
||||
|
||||
* Integer images will have approximately *targetHistogramSize* bins,
|
||||
with each bin having an integer width.
|
||||
* All other types will have *targetHistogramSize* bins.
|
||||
|
||||
This method is also used when automatically computing levels.
|
||||
"""
|
||||
if self.image is None:
|
||||
return None,None
|
||||
stepData = self.image[::step, ::step]
|
||||
hist = np.histogram(stepData, bins=bins)
|
||||
if step == 'auto':
|
||||
step = (np.ceil(self.image.shape[0] / targetImageSize),
|
||||
np.ceil(self.image.shape[1] / targetImageSize))
|
||||
if np.isscalar(step):
|
||||
step = (step, step)
|
||||
stepData = self.image[::step[0], ::step[1]]
|
||||
|
||||
if bins == 'auto':
|
||||
if stepData.dtype.kind in "ui":
|
||||
mn = stepData.min()
|
||||
mx = stepData.max()
|
||||
step = np.ceil((mx-mn) / 500.)
|
||||
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
|
||||
else:
|
||||
bins = 500
|
||||
|
||||
kwds['bins'] = bins
|
||||
hist = np.histogram(stepData, **kwds)
|
||||
|
||||
return hist[1][:-1], hist[0]
|
||||
|
||||
def setPxMode(self, b):
|
||||
|
@ -3,7 +3,7 @@ from .LabelItem import LabelItem
|
||||
from ..Qt import QtGui, QtCore
|
||||
from .. import functions as fn
|
||||
from ..Point import Point
|
||||
from .ScatterPlotItem import ScatterPlotItem
|
||||
from .ScatterPlotItem import ScatterPlotItem, drawSymbol
|
||||
from .PlotDataItem import PlotDataItem
|
||||
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
|
||||
__all__ = ['LegendItem']
|
||||
@ -167,7 +167,7 @@ class ItemSample(GraphicsWidget):
|
||||
size = opts['size']
|
||||
|
||||
p.translate(10,10)
|
||||
path = ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush)
|
||||
path = drawSymbol(p, symbol, size, pen, brush)
|
||||
|
||||
|
||||
|
||||
|
@ -393,16 +393,18 @@ class PlotCurveItem(GraphicsObject):
|
||||
if self.path is None:
|
||||
x,y = self.getData()
|
||||
if x is None or len(x) == 0 or y is None or len(y) == 0:
|
||||
return QtGui.QPainterPath()
|
||||
self.path = self.generatePath(*self.getData())
|
||||
self.path = QtGui.QPainterPath()
|
||||
else:
|
||||
self.path = self.generatePath(*self.getData())
|
||||
self.fillPath = None
|
||||
self._mouseShape = None
|
||||
|
||||
return self.path
|
||||
|
||||
@debug.warnOnException ## raising an exception here causes crash
|
||||
def paint(self, p, opt, widget):
|
||||
profiler = debug.Profiler()
|
||||
if self.xData is None:
|
||||
if self.xData is None or len(self.xData) == 0:
|
||||
return
|
||||
|
||||
if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):
|
||||
|
@ -15,7 +15,7 @@ class PlotDataItem(GraphicsObject):
|
||||
GraphicsItem for displaying plot curves, scatter plots, or both.
|
||||
While it is possible to use :class:`PlotCurveItem <pyqtgraph.PlotCurveItem>` or
|
||||
:class:`ScatterPlotItem <pyqtgraph.ScatterPlotItem>` individually, this class
|
||||
provides a unified interface to both. Inspances of :class:`PlotDataItem` are
|
||||
provides a unified interface to both. Instances of :class:`PlotDataItem` are
|
||||
usually created by plot() methods such as :func:`pyqtgraph.plot` and
|
||||
:func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
|
||||
|
||||
@ -531,15 +531,17 @@ class PlotDataItem(GraphicsObject):
|
||||
## downsampling is expensive; delay until after clipping.
|
||||
|
||||
if self.opts['clipToView']:
|
||||
# this option presumes that x-values have uniform spacing
|
||||
range = self.viewRect()
|
||||
if range is not None:
|
||||
dx = float(x[-1]-x[0]) / (len(x)-1)
|
||||
# clip to visible region extended by downsampling value
|
||||
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
|
||||
x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1)
|
||||
x = x[x0:x1]
|
||||
y = y[x0:x1]
|
||||
view = self.getViewBox()
|
||||
if view is None or not view.autoRangeEnabled()[0]:
|
||||
# this option presumes that x-values have uniform spacing
|
||||
range = self.viewRect()
|
||||
if range is not None:
|
||||
dx = float(x[-1]-x[0]) / (len(x)-1)
|
||||
# clip to visible region extended by downsampling value
|
||||
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
|
||||
x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1)
|
||||
x = x[x0:x1]
|
||||
y = y[x0:x1]
|
||||
|
||||
if ds > 1:
|
||||
if self.opts['downsampleMethod'] == 'subsample':
|
||||
|
@ -95,7 +95,6 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
|
||||
lastFileDir = None
|
||||
managers = {}
|
||||
|
||||
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs):
|
||||
"""
|
||||
@ -369,28 +368,6 @@ class PlotItem(GraphicsWidget):
|
||||
self.scene().removeItem(self.vb)
|
||||
self.vb = None
|
||||
|
||||
## causes invalid index errors:
|
||||
#for i in range(self.layout.count()):
|
||||
#self.layout.removeAt(i)
|
||||
|
||||
#for p in self.proxies:
|
||||
#try:
|
||||
#p.setWidget(None)
|
||||
#except RuntimeError:
|
||||
#break
|
||||
#self.scene().removeItem(p)
|
||||
#self.proxies = []
|
||||
|
||||
#self.menuAction.releaseWidget(self.menuAction.defaultWidget())
|
||||
#self.menuAction.setParent(None)
|
||||
#self.menuAction = None
|
||||
|
||||
#if self.manager is not None:
|
||||
#self.manager.sigWidgetListChanged.disconnect(self.updatePlotList)
|
||||
#self.manager.removeWidget(self.name)
|
||||
#else:
|
||||
#print "no manager"
|
||||
|
||||
def registerPlot(self, name): ## for backward compatibility
|
||||
self.vb.register(name)
|
||||
|
||||
|
@ -1623,9 +1623,9 @@ class PolyLineROI(ROI):
|
||||
if pos is None:
|
||||
pos = [0,0]
|
||||
|
||||
ROI.__init__(self, pos, size=[1,1], **args)
|
||||
self.closed = closed
|
||||
self.segments = []
|
||||
ROI.__init__(self, pos, size=[1,1], **args)
|
||||
|
||||
for p in positions:
|
||||
self.addFreeHandle(p)
|
||||
@ -1750,6 +1750,10 @@ class PolyLineROI(ROI):
|
||||
shape[axes[1]] = sliced.shape[axes[1]]
|
||||
return sliced * mask.reshape(shape)
|
||||
|
||||
def setPen(self, *args, **kwds):
|
||||
ROI.setPen(self, *args, **kwds)
|
||||
for seg in self.segments:
|
||||
seg.setPen(*args, **kwds)
|
||||
|
||||
class LineSegmentROI(ROI):
|
||||
"""
|
||||
|
@ -3,6 +3,11 @@ from ..Point import Point
|
||||
from .. import functions as fn
|
||||
from .GraphicsItem import GraphicsItem
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from itertools import starmap, repeat
|
||||
try:
|
||||
from itertools import imap
|
||||
except ImportError:
|
||||
imap = map
|
||||
import numpy as np
|
||||
import weakref
|
||||
from .. import getConfigOption
|
||||
@ -86,11 +91,8 @@ class SymbolAtlas(object):
|
||||
pm = atlas.getAtlas()
|
||||
|
||||
"""
|
||||
class SymbolCoords(list): ## needed because lists are not allowed in weak references.
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
# symbol key : [x, y, w, h] atlas coordinates
|
||||
# symbol key : QRect(...) coordinates where symbol can be found in atlas.
|
||||
# note that the coordinate list will always be the same list object as
|
||||
# long as the symbol is in the atlas, but the coordinates may
|
||||
# change if the atlas is rebuilt.
|
||||
@ -101,28 +103,32 @@ class SymbolAtlas(object):
|
||||
self.atlasData = None # numpy array of atlas image
|
||||
self.atlas = None # atlas as QPixmap
|
||||
self.atlasValid = False
|
||||
self.max_width=0
|
||||
|
||||
def getSymbolCoords(self, opts):
|
||||
"""
|
||||
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
|
||||
"""
|
||||
coords = np.empty(len(opts), dtype=object)
|
||||
sourceRect = np.empty(len(opts), dtype=object)
|
||||
keyi = None
|
||||
sourceRecti = None
|
||||
for i, rec in enumerate(opts):
|
||||
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush']
|
||||
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen
|
||||
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush
|
||||
key = (symbol, size, fn.colorTuple(pen.color()), pen.widthF(), pen.style(), fn.colorTuple(brush.color()))
|
||||
if key not in self.symbolMap:
|
||||
newCoords = SymbolAtlas.SymbolCoords()
|
||||
self.symbolMap[key] = newCoords
|
||||
self.atlasValid = False
|
||||
#try:
|
||||
#self.addToAtlas(key) ## squeeze this into the atlas if there is room
|
||||
#except:
|
||||
#self.buildAtlas() ## otherwise, we need to rebuild
|
||||
|
||||
coords[i] = self.symbolMap[key]
|
||||
return coords
|
||||
key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes?
|
||||
if key == keyi:
|
||||
sourceRect[i] = sourceRecti
|
||||
else:
|
||||
try:
|
||||
sourceRect[i] = self.symbolMap[key]
|
||||
except KeyError:
|
||||
newRectSrc = QtCore.QRectF()
|
||||
newRectSrc.pen = rec['pen']
|
||||
newRectSrc.brush = rec['brush']
|
||||
self.symbolMap[key] = newRectSrc
|
||||
self.atlasValid = False
|
||||
sourceRect[i] = newRectSrc
|
||||
keyi = key
|
||||
sourceRecti = newRectSrc
|
||||
return sourceRect
|
||||
|
||||
def buildAtlas(self):
|
||||
# get rendered array for all symbols, keep track of avg/max width
|
||||
@ -130,15 +136,13 @@ class SymbolAtlas(object):
|
||||
avgWidth = 0.0
|
||||
maxWidth = 0
|
||||
images = []
|
||||
for key, coords in self.symbolMap.items():
|
||||
if len(coords) == 0:
|
||||
pen = fn.mkPen(color=key[2], width=key[3], style=key[4])
|
||||
brush = fn.mkBrush(color=key[5])
|
||||
img = renderSymbol(key[0], key[1], pen, brush)
|
||||
for key, sourceRect in self.symbolMap.items():
|
||||
if sourceRect.width() == 0:
|
||||
img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush)
|
||||
images.append(img) ## we only need this to prevent the images being garbage collected immediately
|
||||
arr = fn.imageToArray(img, copy=False, transpose=False)
|
||||
else:
|
||||
(x,y,w,h) = self.symbolMap[key]
|
||||
(y,x,h,w) = sourceRect.getRect()
|
||||
arr = self.atlasData[x:x+w, y:y+w]
|
||||
rendered[key] = arr
|
||||
w = arr.shape[0]
|
||||
@ -169,17 +173,18 @@ class SymbolAtlas(object):
|
||||
x = 0
|
||||
rowheight = h
|
||||
self.atlasRows.append([y, rowheight, 0])
|
||||
self.symbolMap[key][:] = x, y, w, h
|
||||
self.symbolMap[key].setRect(y, x, h, w)
|
||||
x += w
|
||||
self.atlasRows[-1][2] = x
|
||||
height = y + rowheight
|
||||
|
||||
self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte)
|
||||
for key in symbols:
|
||||
x, y, w, h = self.symbolMap[key]
|
||||
y, x, h, w = self.symbolMap[key].getRect()
|
||||
self.atlasData[x:x+w, y:y+h] = rendered[key]
|
||||
self.atlas = None
|
||||
self.atlasValid = True
|
||||
self.max_width = maxWidth
|
||||
|
||||
def getAtlas(self):
|
||||
if not self.atlasValid:
|
||||
@ -223,10 +228,9 @@ class ScatterPlotItem(GraphicsObject):
|
||||
GraphicsObject.__init__(self)
|
||||
|
||||
self.picture = None # QPicture used for rendering when pxmode==False
|
||||
self.fragments = None # fragment specification for pxmode; updated every time the view changes.
|
||||
self.fragmentAtlas = SymbolAtlas()
|
||||
|
||||
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object), ('item', object)])
|
||||
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)])
|
||||
self.bounds = [None, None] ## caches data bounds
|
||||
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
|
||||
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
|
||||
@ -237,8 +241,8 @@ class ScatterPlotItem(GraphicsObject):
|
||||
'name': None,
|
||||
}
|
||||
|
||||
self.setPen(200,200,200, update=False)
|
||||
self.setBrush(100,100,150, update=False)
|
||||
self.setPen(fn.mkPen(getConfigOption('foreground')), update=False)
|
||||
self.setBrush(fn.mkBrush(100,100,150), update=False)
|
||||
self.setSymbol('o', update=False)
|
||||
self.setSize(7, update=False)
|
||||
profiler()
|
||||
@ -388,6 +392,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.setPointData(kargs['data'], dataSet=newData)
|
||||
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
self.bounds = [None, None]
|
||||
self.invalidate()
|
||||
self.updateSpots(newData)
|
||||
@ -396,12 +401,10 @@ class ScatterPlotItem(GraphicsObject):
|
||||
def invalidate(self):
|
||||
## clear any cached drawing state
|
||||
self.picture = None
|
||||
self.fragments = None
|
||||
self.update()
|
||||
|
||||
def getData(self):
|
||||
return self.data['x'], self.data['y']
|
||||
|
||||
return self.data['x'], self.data['y']
|
||||
|
||||
def setPoints(self, *args, **kargs):
|
||||
##Deprecated; use setData
|
||||
@ -434,7 +437,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
else:
|
||||
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['sourceRect'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -459,7 +462,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
||||
#self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['sourceRect'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -482,7 +485,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['symbol'] = symbol
|
||||
self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['sourceRect'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -505,7 +508,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['size'] = size
|
||||
self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['sourceRect'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -537,22 +540,26 @@ class ScatterPlotItem(GraphicsObject):
|
||||
def updateSpots(self, dataSet=None):
|
||||
if dataSet is None:
|
||||
dataSet = self.data
|
||||
self._maxSpotWidth = 0
|
||||
self._maxSpotPxWidth = 0
|
||||
|
||||
invalidate = False
|
||||
self.measureSpotSizes(dataSet)
|
||||
if self.opts['pxMode']:
|
||||
mask = np.equal(dataSet['fragCoords'], None)
|
||||
mask = np.equal(dataSet['sourceRect'], None)
|
||||
if np.any(mask):
|
||||
invalidate = True
|
||||
opts = self.getSpotOpts(dataSet[mask])
|
||||
coords = self.fragmentAtlas.getSymbolCoords(opts)
|
||||
dataSet['fragCoords'][mask] = coords
|
||||
sourceRect = self.fragmentAtlas.getSymbolCoords(opts)
|
||||
dataSet['sourceRect'][mask] = sourceRect
|
||||
|
||||
#for rec in dataSet:
|
||||
#if rec['fragCoords'] is None:
|
||||
#invalidate = True
|
||||
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
|
||||
self.fragmentAtlas.getAtlas() # generate atlas so source widths are available.
|
||||
|
||||
dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2
|
||||
dataSet['targetRect'] = None
|
||||
self._maxSpotPxWidth = self.fragmentAtlas.max_width
|
||||
else:
|
||||
self._maxSpotWidth = 0
|
||||
self._maxSpotPxWidth = 0
|
||||
self.measureSpotSizes(dataSet)
|
||||
|
||||
if invalidate:
|
||||
self.invalidate()
|
||||
|
||||
@ -669,29 +676,42 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.prepareGeometryChange()
|
||||
GraphicsObject.viewTransformChanged(self)
|
||||
self.bounds = [None, None]
|
||||
self.fragments = None
|
||||
|
||||
def generateFragments(self):
|
||||
tr = self.deviceTransform()
|
||||
if tr is None:
|
||||
return
|
||||
pts = np.empty((2,len(self.data['x'])))
|
||||
pts[0] = self.data['x']
|
||||
pts[1] = self.data['y']
|
||||
pts = fn.transformCoordinates(tr, pts)
|
||||
self.fragments = []
|
||||
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
|
||||
## Still won't be able to render correctly, though.
|
||||
for i in xrange(len(self.data)):
|
||||
rec = self.data[i]
|
||||
pos = QtCore.QPointF(pts[0,i], pts[1,i])
|
||||
x,y,w,h = rec['fragCoords']
|
||||
rect = QtCore.QRectF(y, x, h, w)
|
||||
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
|
||||
|
||||
self.data['targetRect'] = None
|
||||
|
||||
def setExportMode(self, *args, **kwds):
|
||||
GraphicsObject.setExportMode(self, *args, **kwds)
|
||||
self.invalidate()
|
||||
|
||||
|
||||
def mapPointsToDevice(self, pts):
|
||||
# Map point locations to device
|
||||
tr = self.deviceTransform()
|
||||
if tr is None:
|
||||
return None
|
||||
|
||||
#pts = np.empty((2,len(self.data['x'])))
|
||||
#pts[0] = self.data['x']
|
||||
#pts[1] = self.data['y']
|
||||
pts = fn.transformCoordinates(tr, pts)
|
||||
pts -= self.data['width']
|
||||
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
|
||||
|
||||
return pts
|
||||
|
||||
def getViewMask(self, pts):
|
||||
# Return bool mask indicating all points that are within viewbox
|
||||
# pts is expressed in *device coordiantes*
|
||||
vb = self.getViewBox()
|
||||
if vb is None:
|
||||
return None
|
||||
viewBounds = vb.mapRectToDevice(vb.boundingRect())
|
||||
w = self.data['width']
|
||||
mask = ((pts[0] + w > viewBounds.left()) &
|
||||
(pts[0] - w < viewBounds.right()) &
|
||||
(pts[1] + w > viewBounds.top()) &
|
||||
(pts[1] - w < viewBounds.bottom())) ## remove out of view points
|
||||
return mask
|
||||
|
||||
|
||||
@debug.warnOnException ## raising an exception here causes crash
|
||||
def paint(self, p, *args):
|
||||
@ -707,29 +727,44 @@ class ScatterPlotItem(GraphicsObject):
|
||||
scale = 1.0
|
||||
|
||||
if self.opts['pxMode'] is True:
|
||||
atlas = self.fragmentAtlas.getAtlas()
|
||||
#arr = fn.imageToArray(atlas.toImage(), copy=True)
|
||||
#if hasattr(self, 'lastAtlas'):
|
||||
#if np.any(self.lastAtlas != arr):
|
||||
#print "Atlas changed:", arr
|
||||
#self.lastAtlas = arr
|
||||
|
||||
if self.fragments is None:
|
||||
self.updateSpots()
|
||||
self.generateFragments()
|
||||
|
||||
p.resetTransform()
|
||||
|
||||
if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False:
|
||||
p.drawPixmapFragments(self.fragments, atlas)
|
||||
else:
|
||||
p.setRenderHint(p.Antialiasing, aa)
|
||||
# Map point coordinates to device
|
||||
pts = np.vstack([self.data['x'], self.data['y']])
|
||||
pts = self.mapPointsToDevice(pts)
|
||||
if pts is None:
|
||||
return
|
||||
|
||||
# Cull points that are outside view
|
||||
viewMask = self.getViewMask(pts)
|
||||
#pts = pts[:,mask]
|
||||
#data = self.data[mask]
|
||||
|
||||
if self.opts['useCache'] and self._exportOpts is False:
|
||||
# Draw symbols from pre-rendered atlas
|
||||
atlas = self.fragmentAtlas.getAtlas()
|
||||
|
||||
for i in range(len(self.data)):
|
||||
rec = self.data[i]
|
||||
frag = self.fragments[i]
|
||||
# Update targetRects if necessary
|
||||
updateMask = viewMask & np.equal(self.data['targetRect'], None)
|
||||
if np.any(updateMask):
|
||||
updatePts = pts[:,updateMask]
|
||||
width = self.data[updateMask]['width']*2
|
||||
self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width))
|
||||
|
||||
data = self.data[viewMask]
|
||||
if USE_PYSIDE:
|
||||
list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect']))
|
||||
else:
|
||||
p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas)
|
||||
else:
|
||||
# render each symbol individually
|
||||
p.setRenderHint(p.Antialiasing, aa)
|
||||
|
||||
data = self.data[viewMask]
|
||||
pts = pts[:,viewMask]
|
||||
for i, rec in enumerate(data):
|
||||
p.resetTransform()
|
||||
p.translate(frag.x, frag.y)
|
||||
p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width'])
|
||||
drawSymbol(p, *self.getSpotOpts(rec, scale))
|
||||
else:
|
||||
if self.picture is None:
|
||||
@ -891,7 +926,7 @@ class SpotItem(object):
|
||||
self._data['data'] = data
|
||||
|
||||
def updateItem(self):
|
||||
self._data['fragCoords'] = None
|
||||
self._data['sourceRect'] = None
|
||||
self._plot.updateSpots(self._data.reshape(1))
|
||||
self._plot.invalidate()
|
||||
|
||||
|
23
graphicsItems/tests/ScatterPlotItem.py
Normal file
23
graphicsItems/tests/ScatterPlotItem.py
Normal file
@ -0,0 +1,23 @@
|
||||
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()
|
@ -198,6 +198,7 @@ class ImageView(QtGui.QWidget):
|
||||
if not isinstance(img, np.ndarray):
|
||||
raise Exception("Image must be specified as ndarray.")
|
||||
self.image = img
|
||||
self.imageDisp = None
|
||||
|
||||
if xvals is not None:
|
||||
self.tVals = xvals
|
||||
|
@ -1,41 +1,212 @@
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..SignalProxy import SignalProxy
|
||||
|
||||
from ..pgcollections import OrderedDict
|
||||
from ..python2_3 import asUnicode
|
||||
|
||||
class ComboBox(QtGui.QComboBox):
|
||||
"""Extends QComboBox to add extra functionality.
|
||||
- updateList() - updates the items in the comboBox while blocking signals, remembers and resets to the previous values if it's still in the list
|
||||
|
||||
* Handles dict mappings -- user selects a text key, and the ComboBox indicates
|
||||
the selected value.
|
||||
* Requires item strings to be unique
|
||||
* Remembers selected value if list is cleared and subsequently repopulated
|
||||
* setItems() replaces the items in the ComboBox and blocks signals if the
|
||||
value ultimately does not change.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, parent=None, items=None, default=None):
|
||||
QtGui.QComboBox.__init__(self, parent)
|
||||
self.currentIndexChanged.connect(self.indexChanged)
|
||||
self._ignoreIndexChange = False
|
||||
|
||||
#self.value = default
|
||||
self._chosenText = None
|
||||
self._items = OrderedDict()
|
||||
|
||||
if items is not None:
|
||||
self.addItems(items)
|
||||
self.setItems(items)
|
||||
if default is not None:
|
||||
self.setValue(default)
|
||||
|
||||
def setValue(self, value):
|
||||
ind = self.findText(value)
|
||||
"""Set the selected item to the first one having the given value."""
|
||||
text = None
|
||||
for k,v in self._items.items():
|
||||
if v == value:
|
||||
text = k
|
||||
break
|
||||
if text is None:
|
||||
raise ValueError(value)
|
||||
|
||||
self.setText(text)
|
||||
|
||||
def setText(self, text):
|
||||
"""Set the selected item to the first one having the given text."""
|
||||
ind = self.findText(text)
|
||||
if ind == -1:
|
||||
return
|
||||
raise ValueError(text)
|
||||
#self.value = value
|
||||
self.setCurrentIndex(ind)
|
||||
|
||||
def updateList(self, items):
|
||||
prevVal = str(self.currentText())
|
||||
try:
|
||||
self.setCurrentIndex(ind)
|
||||
|
||||
def value(self):
|
||||
"""
|
||||
If items were given as a list of strings, then return the currently
|
||||
selected text. If items were given as a dict, then return the value
|
||||
corresponding to the currently selected key. If the combo list is empty,
|
||||
return None.
|
||||
"""
|
||||
if self.count() == 0:
|
||||
return None
|
||||
text = asUnicode(self.currentText())
|
||||
return self._items[text]
|
||||
|
||||
def ignoreIndexChange(func):
|
||||
# Decorator that prevents updates to self._chosenText
|
||||
def fn(self, *args, **kwds):
|
||||
prev = self._ignoreIndexChange
|
||||
self._ignoreIndexChange = True
|
||||
try:
|
||||
ret = func(self, *args, **kwds)
|
||||
finally:
|
||||
self._ignoreIndexChange = prev
|
||||
return ret
|
||||
return fn
|
||||
|
||||
def blockIfUnchanged(func):
|
||||
# decorator that blocks signal emission during complex operations
|
||||
# and emits currentIndexChanged only if the value has actually
|
||||
# changed at the end.
|
||||
def fn(self, *args, **kwds):
|
||||
prevVal = self.value()
|
||||
blocked = self.signalsBlocked()
|
||||
self.blockSignals(True)
|
||||
try:
|
||||
ret = func(self, *args, **kwds)
|
||||
finally:
|
||||
self.blockSignals(blocked)
|
||||
|
||||
# only emit if the value has changed
|
||||
if self.value() != prevVal:
|
||||
self.currentIndexChanged.emit(self.currentIndex())
|
||||
|
||||
return ret
|
||||
return fn
|
||||
|
||||
@ignoreIndexChange
|
||||
@blockIfUnchanged
|
||||
def setItems(self, items):
|
||||
"""
|
||||
*items* may be a list or a dict.
|
||||
If a dict is given, then the keys are used to populate the combo box
|
||||
and the values will be used for both value() and setValue().
|
||||
"""
|
||||
prevVal = self.value()
|
||||
|
||||
self.blockSignals(True)
|
||||
try:
|
||||
self.clear()
|
||||
self.addItems(items)
|
||||
self.setValue(prevVal)
|
||||
|
||||
finally:
|
||||
self.blockSignals(False)
|
||||
|
||||
if str(self.currentText()) != prevVal:
|
||||
# only emit if we were not able to re-set the original value
|
||||
if self.value() != prevVal:
|
||||
self.currentIndexChanged.emit(self.currentIndex())
|
||||
|
||||
|
||||
def items(self):
|
||||
return self.items.copy()
|
||||
|
||||
def updateList(self, items):
|
||||
# for backward compatibility
|
||||
return self.setItems(items)
|
||||
|
||||
def indexChanged(self, index):
|
||||
# current index has changed; need to remember new 'chosen text'
|
||||
if self._ignoreIndexChange:
|
||||
return
|
||||
self._chosenText = asUnicode(self.currentText())
|
||||
|
||||
def setCurrentIndex(self, index):
|
||||
QtGui.QComboBox.setCurrentIndex(self, index)
|
||||
|
||||
def itemsChanged(self):
|
||||
# try to set the value to the last one selected, if it is available.
|
||||
if self._chosenText is not None:
|
||||
try:
|
||||
self.setText(self._chosenText)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@ignoreIndexChange
|
||||
def insertItem(self, *args):
|
||||
raise NotImplementedError()
|
||||
#QtGui.QComboBox.insertItem(self, *args)
|
||||
#self.itemsChanged()
|
||||
|
||||
@ignoreIndexChange
|
||||
def insertItems(self, *args):
|
||||
raise NotImplementedError()
|
||||
#QtGui.QComboBox.insertItems(self, *args)
|
||||
#self.itemsChanged()
|
||||
|
||||
@ignoreIndexChange
|
||||
def addItem(self, *args, **kwds):
|
||||
# Need to handle two different function signatures for QComboBox.addItem
|
||||
try:
|
||||
if isinstance(args[0], basestring):
|
||||
text = args[0]
|
||||
if len(args) == 2:
|
||||
value = args[1]
|
||||
else:
|
||||
value = kwds.get('value', text)
|
||||
else:
|
||||
text = args[1]
|
||||
if len(args) == 3:
|
||||
value = args[2]
|
||||
else:
|
||||
value = kwds.get('value', text)
|
||||
|
||||
except IndexError:
|
||||
raise TypeError("First or second argument of addItem must be a string.")
|
||||
|
||||
if text in self._items:
|
||||
raise Exception('ComboBox already has item named "%s".' % text)
|
||||
|
||||
self._items[text] = value
|
||||
QtGui.QComboBox.addItem(self, *args)
|
||||
self.itemsChanged()
|
||||
|
||||
def setItemValue(self, name, value):
|
||||
if name not in self._items:
|
||||
self.addItem(name, value)
|
||||
else:
|
||||
self._items[name] = value
|
||||
|
||||
@ignoreIndexChange
|
||||
@blockIfUnchanged
|
||||
def addItems(self, items):
|
||||
if isinstance(items, list):
|
||||
texts = items
|
||||
items = dict([(x, x) for x in items])
|
||||
elif isinstance(items, dict):
|
||||
texts = list(items.keys())
|
||||
else:
|
||||
raise TypeError("items argument must be list or dict (got %s)." % type(items))
|
||||
|
||||
for t in texts:
|
||||
if t in self._items:
|
||||
raise Exception('ComboBox already has item named "%s".' % t)
|
||||
|
||||
|
||||
for k,v in items.items():
|
||||
self._items[k] = v
|
||||
QtGui.QComboBox.addItems(self, list(texts))
|
||||
|
||||
self.itemsChanged()
|
||||
|
||||
@ignoreIndexChange
|
||||
def clear(self):
|
||||
self._items = OrderedDict()
|
||||
QtGui.QComboBox.clear(self)
|
||||
self.itemsChanged()
|
||||
|
||||
|
44
widgets/tests/test_combobox.py
Normal file
44
widgets/tests/test_combobox.py
Normal file
@ -0,0 +1,44 @@
|
||||
import pyqtgraph as pg
|
||||
pg.mkQApp()
|
||||
|
||||
def test_combobox():
|
||||
cb = pg.ComboBox()
|
||||
items = {'a': 1, 'b': 2, 'c': 3}
|
||||
cb.setItems(items)
|
||||
cb.setValue(2)
|
||||
assert str(cb.currentText()) == 'b'
|
||||
assert cb.value() == 2
|
||||
|
||||
# Clear item list; value should be None
|
||||
cb.clear()
|
||||
assert cb.value() == None
|
||||
|
||||
# Reset item list; value should be set automatically
|
||||
cb.setItems(items)
|
||||
assert cb.value() == 2
|
||||
|
||||
# Clear item list; repopulate with same names and new values
|
||||
items = {'a': 4, 'b': 5, 'c': 6}
|
||||
cb.clear()
|
||||
cb.setItems(items)
|
||||
assert cb.value() == 5
|
||||
|
||||
# Set list instead of dict
|
||||
cb.setItems(list(items.keys()))
|
||||
assert str(cb.currentText()) == 'b'
|
||||
|
||||
cb.setValue('c')
|
||||
assert cb.value() == str(cb.currentText())
|
||||
assert cb.value() == 'c'
|
||||
|
||||
cb.setItemValue('c', 7)
|
||||
assert cb.value() == 7
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cb = pg.ComboBox()
|
||||
cb.show()
|
||||
cb.setItems({'': None, 'a': 1, 'b': 2, 'c': 3})
|
||||
def fn(ind):
|
||||
print("New value: %s" % cb.value())
|
||||
cb.currentIndexChanged.connect(fn)
|
Loading…
Reference in New Issue
Block a user