from . import edit, label as widget_label, combo_box, button as widget_button
from . import container
from ...thread import threadprop, controller
from .. import value_handling
from ...utils import py3, dictionary
from .. import QtWidgets
import collections
import contextlib
[docs]
class ParamTable(container.QWidgetContainer):
"""
GUI parameter table.
Simplifies creating code-generated controls and displays table layouts.
Has methods for adding various kinds of controls (labels, edit boxes, combo boxes, check boxes),
automatically creates values table for easy settings/getting.
By default supports 2-column (label-control) and 3-column (label-control-indicator) layout, depending on the parameters given to :meth:`setup`.
Similar to :class:`.GUIValues`, has three container-like accessor:
``.h`` for getting the value handler
(i.e., ``self.get_handler(name)`` is equivalent to ``self.h[name]``),
``.w`` for getting the underlying widget
(i.e., ``self.get_widget(name)`` is equivalent to ``self.w[name]``),
``.v`` for settings/getting values using the default getting method
(equivalent to ``.wv`` if ``cache_values=False`` in :meth:`setup`, and to ``.cv`` otherwise),
``.wv`` for settings/getting current current widget values without caching
(i.e., ``self.get_value(name)`` is equivalent to ``self.v[name]``, and ``self.set_value(name, value)`` is equivalent to ``self.v[name]=value``),
``.cv`` for settings/getting values using cached value's table for getting
(i.e., ``self.current_values[name]`` is equivalent to ``self.cv[name]``, and ``self.set_value(name, value)`` is equivalent to ``self.cv[name]=value``),
(i.e., ``self.get_value(name)`` is equivalent to ``self.v[name]``, and ``self.set_value(name, value)`` is equivalent to ``self.v[name]=value``),
``.i`` for settings/getting indicator values
(i.e., ``self.get_indicator(name)`` is equivalent to ``self.i[name]``, and ``self.set_indicator(name, value)`` is equivalent to ``self.i[name]=value``)
``.vs`` for getting the value changed Qt signal
(i.e., ``self.get_value_changed_signal(name)`` is equivalent to ``self.s[name]``),
Like most widgets, requires calling :meth:`setup` to set up before usage.
Args:
parent: parent widget
"""
def __init__(self, parent=None, name=None):
self.params={}
super().__init__(parent,name=name)
self.h=dictionary.ItemAccessor(self.get_handler)
self.w=dictionary.ItemAccessor(self.get_widget)
self.iw=dictionary.ItemAccessor(self.get_indicator_widget)
self.lw=dictionary.ItemAccessor(self.get_label_widget)
self.wv=dictionary.ItemAccessor(self.get_value,self.set_value)
self.v=self.wv
self.cv=dictionary.ItemAccessor(lambda name: self.current_values[name],self.set_value)
self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator)
self.vs=dictionary.ItemAccessor(self.get_value_changed_signal)
def _make_new_layout(self, kind, *args, **kwargs):
layout=super()._make_new_layout(kind,*args,**kwargs)
if kind=="grid":
layout.setSpacing(5)
return layout
def _set_main_layout(self):
super()._set_main_layout()
self.main_layout.setContentsMargins(5,5,5,5)
self.main_layout.setColumnStretch(1,1)
[docs]
def setup(self, name=None, add_indicator=True, gui_thread_safe=False, cache_values=False, change_focused_control=False): # pylint: disable=arguments-differ, arguments-renamed
"""
Setup the table.
Args:
name (str): table widget name
add_indicator (bool): if ``True``, add indicators for all added widgets by default.
gui_thread_safe (bool): if ``True``, all value-access and indicator-access calls
(``get/set_value``, ``get/set_all_values``, ``get/set_indicator``, and ``update_indicators``) are automatically called in the GUI thread.
cache_values (bool): if ``True`` or ``"update_one"``, store a dictionary with all the current values and update it every time a GUI value is changed;
provides a thread-safe way to check current parameters without lag
(unlike :meth:`get_value` or :meth:`get_all_values` with ``gui_thread_safe==True``, which re-route calls to a GUI thread and may cause up to 100ms delay)
can also be set to ``"update_all"``, in which case change of any value will cause value update of all variables;
otherwise, change of a value will only cause update of that same value (might potentially miss some value updates for custom controls).
change_focused_control (bool): if ``False`` and :meth:`set_value` method is called while the widget has user focus, ignore the value;
note that :meth:`set_all_values` will still set the widget value.
"""
super().setup(name=name,layout="grid")
self.add_indicator=add_indicator
self.gui_thread_safe=gui_thread_safe
self.change_focused_control=change_focused_control
self.cache_values=cache_values
self.current_values=dictionary.Dictionary()
self.v=self.cv if cache_values else self.wv
@controller.exsafeSlot()
def _update_cache_values(self, name=None, value=None): # pylint: disable=unused-argument
if self.cache_values:
if name is None or self.cache_values=="update_all":
self.current_values=self.get_all_values()
else:
self.current_values[name]=self.get_value(name)
[docs]
def add_sublayout(self, name, kind="grid", location=("next",0,1,"end")):
return super().add_sublayout(name,kind=kind,location=location)
[docs]
@contextlib.contextmanager
def using_new_sublayout(self, name, kind="grid", location=("next",0,1,"end")):
with super().using_new_sublayout(name,kind=kind,location=location):
yield
[docs]
def pad_borders(self, kind="both", stretch=0):
"""
Add expandable paddings on the bottom and/or right border.
`kind` can be ``"bottom"``, ``"right"``, ``"both"``, or ``"none"`` (do nothing).
Note that if more elements are added, they will be placed after the padding, so the table will be padded in the middle.
"""
if kind in ["bottom","both"]:
self.add_padding("vertical",location=("next",0),stretch=stretch)
if kind in ["right","both"]:
self.add_padding("horizontal",location=(0,"next"),stretch=stretch)
[docs]
def add_frame(self, name, layout="vbox", location=("next",0,1,"end"), gui_values_path=True, no_margins=True):
return super().add_frame(name,layout=layout,location=location,gui_values_path=gui_values_path,no_margins=no_margins)
[docs]
def add_group_box(self, name, caption, layout="vbox", location=("next",0,1,"end"), gui_values_path=True, no_margins=True):
return super().add_group_box(name,caption,layout=layout,location=location,gui_values_path=gui_values_path,no_margins=no_margins)
ParamRow=collections.namedtuple("ParamRow",["widget","label","indicator","value_handler","indicator_handler"])
def _add_widget(self, name, params, add_change_event=True):
name=self._normalize_name(name)
self.params[name]=params
if params.widget is not None:
self.add_child(name,params.widget,location="skip",gui_values_path=False)
self.gui_values.add_handler(name,params.value_handler)
if params.indicator_handler:
self.gui_values.add_indicator_handler(name,params.indicator_handler)
if add_change_event:
params.value_handler.connect_value_changed_handler(lambda value: self.contained_value_changed.emit(name,value),only_signal=True)
if self.cache_values:
params.value_handler.connect_value_changed_handler(lambda value: self._update_cache_values(name,value),only_signal=False)
self._update_cache_values()
[docs]
def add_virtual_element(self, name, value=None, multivalued=False, add_indicator=None):
"""
Add a virtual table element.
Doesn't correspond to any actual widget, but behaves very similarly from the application point of view
(its value can be set or read, it has on-change events, it can have indicator).
The element value is simply stored on set and retrieved on get.
If ``multivalued==True``, the internal value is assumed to be complex, so it is forced to be a :class:`.Dictionary` every time it is set.
If ``add_indicator==True``, add default indicator handler as well.
"""
value_handler=value_handling.VirtualValueHandler(value,multivalued=multivalued)
if add_indicator is None:
add_indicator=self.add_indicator
indicator_handler=value_handling.VirtualIndicatorHandler(multivalued=multivalued) if add_indicator else None
self._add_widget(name,self.ParamRow(None,None,None,value_handler,indicator_handler))
[docs]
def add_property_element(self, name, getter=None, setter=None, add_indicator=True):
"""
Add a property value element.
Doesn't correspond to any actual widget, but behaves very similarly from the application point of view;
each time the value is set or get, the corresponding setter and getter methods are called.
If ``add_indicator==True``, add default (stored value) indicator handler as well.
"""
value_handler=value_handling.PropertyValueHandler(getter=getter,setter=setter)
if add_indicator is None:
add_indicator=self.add_indicator
indicator_handler=value_handling.VirtualIndicatorHandler if add_indicator else None
self._add_widget(name,self.ParamRow(None,None,None,value_handler,indicator_handler))
[docs]
def add_check_box(self, name, caption, value=False, label=None, add_indicator=None, location=None, tooltip=None, add_change_event=True, virtual=False):
"""
Add a checkbox to the table.
Args:
name (str): widget name (used to reference its value in the values table)
caption (str): text on the checkbox
value (bool): specifies initial value
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value,add_indicator=add_indicator)
widget=QtWidgets.QCheckBox(self)
widget.setText(caption)
widget.setObjectName(self.name+"_"+name)
widget.setChecked(value)
return self.add_simple_widget(name,widget,label=label,add_indicator=add_indicator,location=location,tooltip=tooltip,add_change_event=add_change_event)
[docs]
def add_text_label(self, name, value="", label=None, location=None, tooltip=None, add_change_event=False, virtual=False):
"""
Add a text label to the table.
Args:
name (str): widget name (used to reference its value in the values table)
value (bool): specifies initial value
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value)
widget=widget_label.TextLabel(self,value=value)
widget.setObjectName(self.name+"_"+name)
return self.add_simple_widget(name,widget,label=label,add_indicator=False,location=location,tooltip=tooltip,add_change_event=add_change_event)
[docs]
def add_enum_label(self, name, options, value=None, out_of_range="error", prep=None, label=None, location=None, tooltip=None, add_change_event=False, virtual=False):
"""
Add a text label to the table.
Args:
name (str): widget name (used to reference its value in the values table)
options (list): dictionary ``{index_value: text}`` which converts values into text
out_of_range (str): behavior when out-of-range value is applied;
can be ``"error"`` (raise error), ``"text"`` (convert value into text), or ``"ignore"`` (keep current value).
prep: a function which takes a single value argument and converts into an option; useful for "fuzzy" options (e.g., when 0 and ``False`` mean the same thing)
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value)
widget=widget_label.EnumLabel(self,options=options,value=value,prep=prep)
widget.set_out_of_range(out_of_range)
widget.setObjectName(self.name+"_"+name)
return self.add_simple_widget(name,widget,label=label,add_indicator=False,location=location,tooltip=tooltip,add_change_event=add_change_event)
[docs]
def add_num_label(self, name, value=0, limiter=None, formatter=None, label=None, tooltip=None, location=None, add_change_event=False, virtual=False):
"""
Add a numerical label to the table.
Args:
name (str): widget name (used to reference its value in the values table)
value (float): specifies initial value
limiter (tuple): tuple ``(upper_limit, lower_limit, action, value_type)`` specifying value limits;
see :func:`.limiter.as_limiter` for details
formatter (tuple): either ``"int"`` (for integer values), or tuple specifying floating value format;
see :func:`.formatter.as_formatter` for details
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value)
widget=widget_label.NumLabel(self,value=value,limiter=limiter,formatter=formatter)
widget.setObjectName(self.name+"_"+name)
return self.add_simple_widget(name,widget,label=label,add_indicator=False,location=location,tooltip=tooltip,add_change_event=add_change_event)
[docs]
def add_text_edit(self, name, value="", label=None, multiline=False, add_indicator=None, location=None, tooltip=None, add_change_event=True, virtual=False):
"""
Add a text edit to the table.
Args:
name (str): widget name (used to reference its value in the values table)
value (bool): specifies initial value
multiline (bool): if ``True``, use multi-line text edit widget; otherwise, use a standard single-line edit
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value,add_indicator=add_indicator)
widget=edit.TextEdit(self,value=value) if not multiline else edit.MultilineTextEdit(self,value=value)
widget.setObjectName(self.name+"_"+name)
return self.add_simple_widget(name,widget,label=label,add_indicator=add_indicator,location=location,tooltip=tooltip,add_change_event=add_change_event)
[docs]
def add_num_edit(self, name, value=None, limiter=None, formatter=None, custom_steps=None, label=None, add_indicator=None, location=None, tooltip=None, add_change_event=True, virtual=False):
"""
Add a numerical edit to the table.
Args:
name (str): widget name (used to reference its value in the values table)
value (bool): specifies initial value
limiter (tuple): tuple ``(upper_limit, lower_limit, action, value_type)`` specifying value limits;
see :meth:`.NumEdit.set_limiter` for details
formatter (tuple): either ``"int"`` (for integer values), or tuple specifying floating value format;
see :meth:`.NumEdit.set_formatter` for details
custom_steps: if not ``None``, can specify custom fixed value steps when up/down keys are pressed with a modifier key (Control, Alt, or Shift)
specifies a dictionary ``{'ctrl':ctrl_step, 'alt':alt_step, 'shift':shift_step}`` with the corresponding steps (missing elements mean that the modifier key is ignored)
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value,add_indicator=add_indicator)
widget=edit.NumEdit(self,value=value,limiter=limiter,formatter=formatter,custom_steps=custom_steps)
widget.setObjectName(self.name+"_"+name)
return self.add_simple_widget(name,widget,label=label,add_indicator=add_indicator,location=location,tooltip=tooltip,add_change_event=add_change_event)
[docs]
def add_progress_bar(self, name, value=None, label=None, location=None, tooltip=None, add_change_event=True, virtual=False):
"""
Add a progress bar to the table.
Args:
name (str): widget name (used to reference its value in the values table)
value (bool): specifies initial value
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value)
widget=QtWidgets.QProgressBar(self)
widget.setObjectName(self.name+"_"+name)
if value is not None:
widget.setValue(value)
return self.add_simple_widget(name,widget,label=label,location=location,tooltip=tooltip,add_change_event=add_change_event)
[docs]
def add_combo_box(self, name, value=None, options=None, index_values=None, out_of_range="reset", label=None, add_indicator=None, location=None, tooltip=None, add_change_event=True, virtual=False):
"""
Add a combo box to the table.
Args:
name (str): widget name (used to reference its value in the values table)
value: specifies initial value
options (list): list of strings specifying box options or a dictionary ``{option: index_value}``
index_values (list): list of values corresponding to box options; if supplied, these values are used when setting/getting values or sending signals;
if `options` is a dictionary, this parameter is ignored
out_of_range (str): behavior when out-of-range value is applied;
can be ``"error"`` (raise error), ``"reset"`` (reset to no-value position), or ``"ignore"`` (keep current value).
virtual (bool): if ``True``, the widget is not added, and a virtual handler is added instead
Rest of the arguments and the return value are the same as :meth:`add_simple_widget`.
"""
if virtual:
return self.add_virtual_element(name,value=value,add_indicator=add_indicator)
widget=combo_box.ComboBox(self)
widget.setObjectName(self.name+"_"+name)
widget.set_out_of_range(action=out_of_range)
if options is not None:
widget.set_options(options,index_values=index_values)
if value is not None:
widget.set_value(value)
else:
index_values=widget.get_index_values()
widget.set_value(index_values[0] if index_values else 0)
return self.add_simple_widget(name,widget,label=label,add_indicator=add_indicator,location=location,tooltip=tooltip,add_change_event=add_change_event)
def _expand_names_list(self, names):
if isinstance(names,py3.anystring):
names=[names]
if names is None:
names=self.params.keys()
return names
[docs]
def set_enabled(self, names=None, enabled=True, include_indicator=True, include_label=True):
"""Enable or disable widgets with the given names (by default, all widgets)"""
for name in self._expand_names_list(names):
name=self._normalize_name(name)
par=self.params[name]
if par.widget is not None:
par.widget.setEnabled(enabled)
if include_label and par.label is not None:
par.label.setEnabled(enabled)
if include_indicator and par.indicator is not None:
par.indicator.setEnabled(enabled)
[docs]
def set_visible(self, names=None, visible=True, include_indicator=True, include_label=True):
"""Show or hide widgets with the given names (by default, all widgets)"""
for name in self._expand_names_list(names):
name=self._normalize_name(name)
par=self.params[name]
if par.widget is not None:
par.widget.setVisible(visible)
if include_label and par.label is not None:
par.label.setVisible(visible)
if include_indicator and par.indicator is not None:
par.indicator.setVisible(visible)
@controller.gui_thread_method
def get_value(self, name=None):
return super().get_value(name=name)
@controller.gui_thread_method
def get_all_values(self):
return super().get_all_values()
@controller.gui_thread_method
def set_value(self, name, value, force=True): # pylint: disable=arguments-differ
"""
Set value of a widget with the given name.
If ``force==True``, force widget value (e.g., ignoring restriction on not changing values of focused widgets)
"""
if force:
return super().set_value(name,value)
if name is None:
self.set_all_values(value,force=force)
name=self._normalize_name(name)
try:
par=self.params[name]
allow_set=par.value_handler.can_set_value(allow_focus=self.change_focused_control)
except KeyError:
allow_set=True
if allow_set:
return super().set_value(name,value)
@controller.gui_thread_method
def set_all_values(self, value, force=True): # pylint: disable=arguments-differ
"""
Set values of all widgets in the table.
If ``force==True``, force widget value (e.g., ignoring restriction on not changing values of focused widgets)
"""
if force:
return super().set_all_values(value)
for n,v in dictionary.as_dictionary(value).iternodes(to_visit="all",topdown=True,include_path=True):
path="/".join(n)
try:
self.set_value(path,v,force=force)
except KeyError:
pass
@controller.gui_thread_method
def update_value(self, name=None):
return super().update_value(name=name)
[docs]
def get_child(self, name):
name=self._normalize_name(name)
if name in self.params:
return self.get_widget(name)
return super().get_child(name)
[docs]
def remove_child(self, name, clear=True):
name=self._normalize_name(name)
if name in self.params:
return self.remove_widget(name)
return super().remove_child(name,clear=clear)
@controller.gui_thread_method
def get_indicator(self, name=None):
return super().get_indicator(name=name)
@controller.gui_thread_method
def get_all_indicators(self):
return super().get_all_indicators()
@controller.gui_thread_method
def set_indicator(self, name, value, ignore_missing=False):
return super().set_indicator(name,value,ignore_missing=ignore_missing)
@controller.gui_thread_method
def set_all_indicators(self, value, ignore_missing=True):
return super().set_all_indicators(value,ignore_missing=ignore_missing)
@controller.gui_thread_method
def update_indicators(self):
return super().update_indicators()
[docs]
def clear(self, disconnect=False): # pylint: disable=arguments-differ
"""
Clear the table (remove all widgets)
If ``disconnect==True``, also disconnect all slots connected to the ``contained_value_changed`` signal.
"""
if self.params:
if disconnect:
try:
self.contained_value_changed.disconnect()
except (TypeError,RuntimeError): # no signals connected
pass
for name in self.params:
self.gui_values.remove_handler(name,remove_indicator=True)
self.params={}
super().clear()
self._update_cache_values()
else:
super().clear()
def __contains__(self, name):
name=self._normalize_name(name)
return name in self.params
[docs]
class StatusTable(ParamTable):
"""
Expansion of :class:`ParamTable` which adds status lines, which automatically subscribe to signals and update values.
"""
[docs]
def setup(self, name=None, add_indicator=True, gui_thread_safe=False, cache_values=False, change_focused_control=False):
super().setup(name=name,add_indicator=add_indicator,gui_thread_safe=gui_thread_safe,cache_values=cache_values,change_focused_control=change_focused_control)
self._status_line_params={}
[docs]
def add_status_line(self, name, label=None, srcs=None, tags=None, filt=None, fmt=None):
"""
Add a status line to the table:
Args:
name (str): widget name (used to reference its value in the values table)
label (str): if not ``None``, specifies label to put in front of the status line
srcs (list): status signal sources
tags (list): status signal tags
filt (list): filter function for the signals
fmt: if not ``None``, specifies a function which takes 3 arguments (signal source, tag, and value) and generates a status line text.
"""
self.add_text_label(name,label=label)
def update_text(src, tag, value):
self.v[name]=fmt(src,tag,value) if fmt is not None else value
self._status_line_params[name]=(srcs,tags,update_text)
threadprop.current_controller().subscribe_sync(update_text,srcs=srcs,tags=tags,filt=filt,limit_queue=10)
[docs]
def update_status_line(self, name, thread=None, path=None):
"""
Update status line to the variable with the given `path` from the thread with the given `thread` name.
If `thread` is ``None``, use ``srcs`` name provided upon creation.
If `path` is ``None``, use ``tags`` name provided upon creation.
"""
srcs,tags,update_text=self._status_line_params[name]
thread=thread or srcs
path=path or tags
try:
ctl=controller.get_controller(thread,sync=False)
value=ctl.get_variable(path)
if value is not None:
self.v[name]=update_text(srcs,tags,value) if update_text is not None else value
except threadprop.NoControllerThreadError:
pass