diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index 72ac384f..66ea4205 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 ..ordereddict 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 = items.keys() + else: + raise TypeError("items argument must be list or dict.") + + 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, 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..300489e0 --- /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(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:", cb.value() + cb.currentIndexChanged.connect(fn) \ No newline at end of file