Merge branch 'flowchart_plotnode_menu' into develop

This commit is contained in:
Luke Campagnola 2013-12-27 22:36:00 -05:00
commit e62c833a0c
5 changed files with 287 additions and 53 deletions

View File

@ -58,11 +58,15 @@ fc.setInput(dataIn=data)
## populate the flowchart with a basic set of processing nodes. ## populate the flowchart with a basic set of processing nodes.
## (usually we let the user do this) ## (usually we let the user do this)
plotList = {'Top Plot': pw1, 'Bottom Plot': pw2}
pw1Node = fc.createNode('PlotWidget', pos=(0, -150)) pw1Node = fc.createNode('PlotWidget', pos=(0, -150))
pw1Node.setPlotList(plotList)
pw1Node.setPlot(pw1) pw1Node.setPlot(pw1)
pw2Node = fc.createNode('PlotWidget', pos=(150, -150)) pw2Node = fc.createNode('PlotWidget', pos=(150, -150))
pw2Node.setPlot(pw2) pw2Node.setPlot(pw2)
pw2Node.setPlotList(plotList)
fNode = fc.createNode('GaussianFilter', pos=(0, 0)) fNode = fc.createNode('GaussianFilter', pos=(0, 0))
fNode.ctrls['sigma'].setValue(5) fNode.ctrls['sigma'].setValue(5)

View File

@ -4,7 +4,7 @@ import weakref
from ...Qt import QtCore, QtGui from ...Qt import QtCore, QtGui
from ...graphicsItems.ScatterPlotItem import ScatterPlotItem from ...graphicsItems.ScatterPlotItem import ScatterPlotItem
from ...graphicsItems.PlotCurveItem import PlotCurveItem from ...graphicsItems.PlotCurveItem import PlotCurveItem
from ... import PlotDataItem from ... import PlotDataItem, ComboBox
from .common import * from .common import *
import numpy as np import numpy as np
@ -16,7 +16,9 @@ class PlotWidgetNode(Node):
def __init__(self, name): def __init__(self, name):
Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}}) 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 = {} self.items = {}
def disconnected(self, localTerm, remoteTerm): def disconnected(self, localTerm, remoteTerm):
@ -26,16 +28,27 @@ class PlotWidgetNode(Node):
def setPlot(self, plot): def setPlot(self, plot):
#print "======set 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.plot = plot
self.updateUi()
self.update()
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
def getPlot(self): def getPlot(self):
return self.plot return self.plot
def process(self, In, display=True): def process(self, In, display=True):
if display: if display and self.plot is not None:
#self.plot.clearPlots()
items = set() items = set()
# Add all new input items to selected plot
for name, vals in In.items(): for name, vals in In.items():
if vals is None: if vals is None:
continue continue
@ -45,14 +58,13 @@ class PlotWidgetNode(Node):
for val in vals: for val in vals:
vid = id(val) vid = id(val)
if vid in self.items and self.items[vid].scene() is self.plot.scene(): 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) items.add(vid)
else: else:
#if isinstance(val, PlotCurveItem): # Add the item to the plot, or generate a new item if needed.
#self.plot.addItem(val)
#item = val
#if isinstance(val, ScatterPlotItem):
#self.plot.addItem(val)
#item = val
if isinstance(val, QtGui.QGraphicsItem): if isinstance(val, QtGui.QGraphicsItem):
self.plot.addItem(val) self.plot.addItem(val)
item = val item = val
@ -60,22 +72,48 @@ class PlotWidgetNode(Node):
item = self.plot.plot(val) item = self.plot.plot(val)
self.items[vid] = item self.items[vid] = item
items.add(vid) items.add(vid)
# Any left-over items that did not appear in the input must be removed
for vid in list(self.items.keys()): for vid in list(self.items.keys()):
if vid not in items: if vid not in items:
#print "remove", self.items[vid]
self.plot.removeItem(self.items[vid]) self.plot.removeItem(self.items[vid])
del self.items[vid] del self.items[vid]
def processBypassed(self, args): def processBypassed(self, args):
if self.plot is None:
return
for item in list(self.items.values()): for item in list(self.items.values()):
self.plot.removeItem(item) self.plot.removeItem(item)
self.items = {} self.items = {}
#def setInput(self, **args): def ctrlWidget(self):
#for k in args: if self.ui is None:
#self.plot.plot(args[k]) 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): class CanvasNode(Node):
"""Connection to a Canvas widget.""" """Connection to a Canvas widget."""

View File

@ -95,7 +95,6 @@ class PlotItem(GraphicsWidget):
lastFileDir = None lastFileDir = None
managers = {}
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): 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.scene().removeItem(self.vb)
self.vb = None 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 def registerPlot(self, name): ## for backward compatibility
self.vb.register(name) self.vb.register(name)

View File

@ -1,41 +1,212 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from ..SignalProxy import SignalProxy from ..SignalProxy import SignalProxy
from ..pgcollections import OrderedDict
from ..python2_3 import asUnicode
class ComboBox(QtGui.QComboBox): class ComboBox(QtGui.QComboBox):
"""Extends QComboBox to add extra functionality. """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): def __init__(self, parent=None, items=None, default=None):
QtGui.QComboBox.__init__(self, parent) 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: if items is not None:
self.addItems(items) self.setItems(items)
if default is not None: if default is not None:
self.setValue(default) self.setValue(default)
def setValue(self, value): 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: if ind == -1:
return raise ValueError(text)
#self.value = value #self.value = value
self.setCurrentIndex(ind) self.setCurrentIndex(ind)
def updateList(self, items): def value(self):
prevVal = str(self.currentText()) """
try: 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) 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.clear()
self.addItems(items) self.addItems(items)
self.setValue(prevVal)
finally: finally:
self.blockSignals(False) 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()) 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()

View 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)