from .. import QtWidgets, Signal
from ..limiter import as_limiter, LimitError, NumberLimit
from ..formatter import as_formatter, str_to_float
[docs]
class TextLabel(QtWidgets.QLabel):
"""
Labview-style text label.
The main difference from the standard ``QLabel`` is the changed event.
"""
def __init__(self, parent, value=None):
super().__init__(parent)
self._value=None
if value is not None:
self.set_value(value)
clicked=Signal()
[docs]
def mousePressEvent(self, ev):
self.clicked.emit()
return super().mousePressEvent(ev)
value_changed=Signal(object)
"""Signal emitted when value is changed"""
[docs]
def get_value(self):
"""Get current numerical value"""
return self._value
[docs]
def set_value(self, value):
"""Set and display current text value"""
if value is not None:
if self._value!=value:
self._value=value
self.setText(str(self._value))
self.value_changed.emit(self._value)
[docs]
class EnumLabel(QtWidgets.QLabel):
"""
Labview-style label for enumerated values.
Can automatically convert input enum values into corresponding text labels based on the `options` dictionary.
Can also specify a function which takes a single value argument and converts into a enum value before checking `options`;
useful for "fuzzy" options (e.g., when 0 and ``False`` mean the same thing)
"""
def __init__(self, parent, options, value=None, prep=None):
super().__init__(parent)
self._value=None
self._options=options if isinstance(options,dict) else dict(zip(range(len(options)),options))
self._out_of_range_action="error"
self._prep=prep
if value is not None:
self.set_value(value)
clicked=Signal()
[docs]
def mousePressEvent(self, ev):
self.clicked.emit()
return super().mousePressEvent(ev)
[docs]
def set_out_of_range(self, action="error"):
"""
Set behavior when out-of-range value is applied.
Can be ``"error"`` (raise error), ``"text"`` (turn value into text and display it), or ``"ignore"`` (keep current value).
"""
if action not in ["error","text","ignore"]:
raise ValueError("unrecognized out-of-range action: {}".format(action))
self._out_of_range_action=action
[docs]
def set_options(self, options, 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.
"""
self._options=options if isinstance(options,dict) else dict(zip(range(len(options)),options))
if value is not None:
self.set_value(value)
elif index is not None:
self.set_value(list(self._options)[index])
else:
self.set_value(self._value)
value_changed=Signal(object)
"""Signal emitted when value is changed"""
[docs]
def get_value(self):
"""Get current numerical value"""
return self._value
def _prepare_value(self, value):
if self._prep is not None:
value=self._prep(value)
if value not in self._options:
if self._out_of_range_action=="ignore":
return None
elif self._out_of_range_action=="error":
raise ValueError("value {} is not among available option {}".format(value,list(self._options)))
else:
value=str(value)
return value
[docs]
def set_value(self, value):
"""Set and display current text value"""
if value is not None:
value=self._prepare_value(value)
if value is None:
return
self.setText(self._options.get(value,value))
if self._value!=value:
self._value=value
self.value_changed.emit(self._value)
[docs]
def repr_value(self, value):
"""Return representation of `value` as a combo box text"""
if value is not None:
value=self._prepare_value(value)
if value is None:
return None
return self._options.get(value,value)
[docs]
class NumLabel(QtWidgets.QLabel):
"""
Labview-style numerical label.
Supports different number representations and metric prefixes.
Args:
parent: parent widget
value: initial value (``None`` means no value is set)
limiter: number limiter (for details, see :meth:`set_limiter`)
formatter: number formatter (for details, see :meth:`set_formatter`)
allow_text: if ``True``, can also take text values (which are displayed as is); otherwise, raise an error.
"""
def __init__(self, parent, value=None, limiter=None, formatter=None, allow_text=True):
super().__init__(parent)
self.limiter=as_limiter(limiter)
self.formatter=as_formatter(formatter)
self._value=None
self.allow_text=allow_text
if value is not None:
self.set_value(value)
clicked=Signal()
[docs]
def mousePressEvent(self, ev):
self.clicked.emit()
return super().mousePressEvent(ev)
[docs]
def set_limiter(self, limiter, new_value=None):
"""
Change current numerical limiter.
Limiter can be a callable object which takes a single value and either returns a limited value, or raises :exc:`.limiter.LimitError` if it should be ignored;
or it can be a tuple ``(lower, upper, action, value_type)``, where ``lower`` and ``upper`` are the limits (``None`` means no limits),
``action`` defines out-of-limit action (either ``"ignore"`` to ignore entered value, or ``"coerce"`` to truncate to the nearest limit),
and ``value_type`` can be ``None`` (keep value as is), ``"float"`` (cast value to float), ``"int"`` (cast value to int).
If the tuple is shorter, the missing parts are filled by default values ``(None, None, "ignore", None)``.
"""
self.limiter=as_limiter(limiter)
if new_value is None:
new_value=self._value
new_value=self._coerce_value(new_value,coerce_on_limit=True)
if new_value!=self._value:
self.set_value(new_value)
def _coerce_value(self, value, coerce_on_limit=False):
for _ in range(10):
str_value=self.formatter(value)
num_value=str_to_float(str_value)
try:
new_value=self.limiter(num_value)
except LimitError:
if coerce_on_limit and isinstance(self.limiter,NumberLimit):
if self.limiter.range[0] is not None and num_value<self.limiter.range[0]:
new_value=self.limiter.range[0]
else:
new_value=self.limiter.range[1]
else:
raise
if new_value==value:
return new_value
value=new_value
raise ValueError("couldn't coerce the new value")
[docs]
def repr_value(self, value):
"""Return representation of `value` according to the current numerical format"""
return self.formatter(value)
value_changed=Signal(object)
"""Signal emitted when value is changed"""
[docs]
def get_value(self):
"""Get current numerical value"""
return self._value
[docs]
def set_value(self, value):
"""Set and display current numerical value"""
if value is not None:
if self._value==value:
return True
try:
if isinstance(value,str):
if self.allow_text:
self._value=value
self.setText(self._value)
else:
raise ValueError("this label doesn't accept text values")
else:
value=self.limiter(value)
self._value=value
self.setText(self.formatter(self._value))
self.value_changed.emit(self._value)
return True
except LimitError:
pass
if self._value is not None:
self.setText(self.formatter(self._value))
return False