from .. import QtWidgets, Signal
[docs]
class ComboBox(QtWidgets.QComboBox):
"""
Expanded combo box.
Maintains internally stored consistent value (which can be, e.g., accessed from different threads).
Allows setting values which are reported via ``value_changed`` signal instead of simple indices.
"""
def __init__(self, parent):
super().__init__(parent)
self.activated.connect(self._on_index_changed)
self._index=-1
self._index_values=None
self._out_of_range_action="error"
self._direct_index_action="ignore"
[docs]
def wheelEvent(self, event):
event.ignore()
[docs]
def set_out_of_range(self, action="error"):
"""
Set behavior when out-of-range value is applied.
Can be ``"error"`` (raise error), ``"reset"`` (reset to no-value position), ``"reset_start"`` (reset to the first position) or ``"ignore"`` (keep current value).
"""
if action not in ["error","reset","reset_start","ignore"]:
raise ValueError("unrecognized out-of-range action: {}".format(action))
self._out_of_range_action=action
[docs]
def set_direct_index_action(self, action="error"):
"""
Set behavior when index values are specified, but direct indexing is used.
Can be ``"ignore"`` (do not allow direct indexing and treat any value as index value),
``"value_default"`` (allow direct indexing, but prioritize index values with the same value),
or ``"index_default"`` (allow direct indexing and prioritize it if index value with the same value exists).
"""
if action not in ["ignore","value_default","index_default"]:
raise ValueError("unrecognized direct index action: {}".format(action))
self._direct_index_action=action
[docs]
def index_to_value(self, idx):
"""Turn numerical index into value"""
if (self._index_values is None) or (idx<0) or (idx>=len(self._index_values)):
return idx
else:
return self._index_values[idx]
[docs]
def value_to_index(self, value):
"""Turn value into a numerical index"""
try:
if value==-1 or self._index_values is None:
return value
if isinstance(value,int) and value>=0 and value<len(self._index_values):
if self._direct_index_action=="value_default" and value in self._index_values:
return self._index_values.index(value)
if self._direct_index_action!="ignore":
return value
return self._index_values.index(value)
except ValueError as err:
if self._out_of_range_action=="error":
raise ValueError("value {} is not among available options {}".format(value,self._index_values)) from err
if self._out_of_range_action=="reset_start":
return 0
return -1
def _on_index_changed(self, index):
if self._index!=index:
self._index=index
self.value_changed.emit(self.index_to_value(self._index))
[docs]
def set_index_values(self, index_values, value=None, index=None):
"""
Set a list of values corresponding to combo box indices.
Can be either a list of values, whose length must be equal to the number of options, or ``None`` (simply use indices).
Note: if the number of combo box options changed (e.g., using ``addItem`` or ``insertItem`` methods),
the index values need to be manually updated; otherwise, the errors might arise if the index is larger than the number of values.
If `value` is specified, set as the new values.
If `index` is specified, use it as the index of a new value; if both `value` and `index` are specified, the `value` takes priority.
"""
if index_values is not None:
if len(index_values)!=self.count():
raise ValueError("number of values {} is different from the number of options {}".format(len(index_values),self.count()))
if -1 in index_values:
raise ValueError("index values {} contain -1, which is reserved to represent no selection".format(index_values))
curr_value=self.get_value()
self._index_values=index_values
if value is not None:
self.set_value(value)
elif index is not None:
self.set_value(self.get_index_values()[index])
else:
self._index=-1
self.setCurrentIndex(-1)
try:
self.set_value(curr_value)
except ValueError:
pass
[docs]
def get_index_values(self):
"""Return the list of values corresponding to combo box indices"""
return list(self._index_values) if self._index_values is not None else list(range(self.count()))
[docs]
def get_options(self):
"""Return the list of labels corresponding to combo box indices"""
return [self.itemText(i) for i in range(self.count())]
[docs]
def get_options_dict(self):
"""Return the dictionary ``{value: label}`` of the option labels"""
return dict(zip(self.get_index_values(),self.get_options()))
[docs]
def set_options(self, options, index_values=None, value=None, index=None):
"""
Set new set of options.
If `index_values` is not ``None``, set these as the new index values; otherwise, index values are reset.
If `options` is a dictionary, interpret it as a mapping ``{option: index_value}``.
If `value` is specified, set as the new values.
If `index` is specified, use it as the index of a new value; if both `value` and `index` are specified, the `value` takes priority.
"""
while self.count():
self.removeItem(0)
if isinstance(options,dict):
index_values=list(options)
options=[options[v] for v in index_values]
self.addItems(options)
self.set_index_values(index_values,value=value,index=index)
[docs]
def insert_option(self, option, index_value=None, index=None):
"""
Insert or append a new option to the list
Insertion (i.e., ``index is not None``) only works for index-valued combo boxes.
"""
if self._index_values is None:
if index is not None:
raise ValueError("insertion only works for index-valued combo boxes")
self.set_options(self.get_options()+[option])
else:
if index_value is None:
raise ValueError("can not add None-valued element")
options,index_values=self.get_options(),self.get_index_values()
index=len(index_values) if index is None else index
options.insert(index,option)
index_values.insert(index,index_value)
self.set_options(options,index_values)
value_changed=Signal(object)
"""Signal emitted when value is changed"""
[docs]
def get_value(self):
"""Get current numerical value"""
return self.index_to_value(self._index)
[docs]
def set_value(self, value, notify_value_change=True):
"""
Set current value.
If ``notify_value_change==True``, emit the `value_changed` signal; otherwise, change value silently.
"""
if not self.count() or value==-1:
return False
index=self.value_to_index(value)
if self._out_of_range_action=="ignore" and index==-1:
return False
index=max(-1,min(index,self.count()-1))
if self._index!=index:
self._index=index
self.setCurrentIndex(self._index)
if notify_value_change:
self.value_changed.emit(self.index_to_value(self._index))
return True
else:
return False
[docs]
def repr_value(self, value):
"""Return representation of `value` as a combo box text"""
if value==-1:
return "N/A"
index=self.value_to_index(value)
return self.itemText(index)