diff --git a/examples/Flowchart.py b/examples/Flowchart.py index 09ea1f93..86c2564b 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -58,11 +58,15 @@ fc.setInput(dataIn=data) ## populate the flowchart with a basic set of processing nodes. ## (usually we let the user do this) +plotList = {'Top Plot': pw1, 'Bottom Plot': pw2} + pw1Node = fc.createNode('PlotWidget', pos=(0, -150)) +pw1Node.setPlotList(plotList) pw1Node.setPlot(pw1) pw2Node = fc.createNode('PlotWidget', pos=(150, -150)) pw2Node.setPlot(pw2) +pw2Node.setPlotList(plotList) fNode = fc.createNode('GaussianFilter', pos=(0, 0)) fNode.ctrls['sigma'].setValue(5) diff --git a/pyqtgraph/flowchart/library/Display.py b/pyqtgraph/flowchart/library/Display.py index 2c352fb2..642e6491 100644 --- a/pyqtgraph/flowchart/library/Display.py +++ b/pyqtgraph/flowchart/library/Display.py @@ -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.""" diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index baff1aa9..575a1599 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -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) diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index 72ac384f..f9983c97 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -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()) - \ No newline at end of file + + 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() + diff --git a/pyqtgraph/widgets/tests/test_combobox.py b/pyqtgraph/widgets/tests/test_combobox.py new file mode 100644 index 00000000..f511331c --- /dev/null +++ b/pyqtgraph/widgets/tests/test_combobox.py @@ -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) \ No newline at end of file