From 2c2135a49f68eb5135fc72ee78675dc7cbbe8cd0 Mon Sep 17 00:00:00 2001
From: Luke Campagnola <luke.campagnola@gmail.com>
Date: Fri, 27 Dec 2013 21:06:31 -0500
Subject: [PATCH] Major updates to ComboBox: - Essentially a graphical
 interface to dict; all items have text and value - Assigns
 previously-selected text after list is cleared and repopulated - Get, set
 current value

---
 pyqtgraph/widgets/ComboBox.py            | 199 +++++++++++++++++++++--
 pyqtgraph/widgets/tests/test_combobox.py |  44 +++++
 2 files changed, 229 insertions(+), 14 deletions(-)
 create mode 100644 pyqtgraph/widgets/tests/test_combobox.py

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)    
+        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()
         
-    def updateList(self, items):
-        prevVal = str(self.currentText())
+        self.blockSignals(True)
         try:
-            self.blockSignals(True)
             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
-- 
GitLab