from pyqtgraph.Qt import QtWidgets, QtCore
from functools import wraps
import numpy as np
[docs]
class BaseParam(QtWidgets.QWidget):
"""
A param object represents a gettable/settable value within a :class:`~puzzlepiece.piece.Piece`. This can be
a general variable for some operation, a setting of a physical device (like laser power), a readout from a
physical device (counts from a spectrometer), or something that can be measured and set (the position of a
movable stage).
A general param is just a variable with a GUI presence (if needed) that can be read from and written to by
Pieces. It can then have getter and setter functions that are called when you get/set the value of the param.
To set and get the value of the param, use the :func:`~puzzlepiece.param.BaseParam.set_value` and
:func:`~puzzlepiece.param.get_value` methods.
**In most cases, you will not instantiate a param directly, but rather use decorators defined within this module
(like** :func:`puzzlepiece.param.spinbox` **)
to register params within a Piece's** :func:`~puzzlepiece.piece.Piece.define_params` **method.**
:param name: A unique (per Piece) id name for the param
:param value: Default value (can be None)
:param setter: A function to be called when setting the param, taking the value as argument. It may return a new value.
:param getter: A function to be called when obtaining the value of the param, raturning the new value.
:param visible: If True, the Piece will generate display a GUI component for the param.
:param format: Default format for displaying the param if a custom input is not defined, and in
:func:`puzzplepiece.parse.format`. For example `{:.2f}`, see https://pyformat.info/ for further details.
:param _type: int, float, str etc - the type of the param value. Can be inferred if a default value is passed.
"""
#: A Qt signal emitted when the value changes
changed = QtCore.Signal()
_type = None
def __init__(
self,
name,
value,
setter=None,
getter=None,
visible=True,
format="{}",
_type=None,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self._setter = setter
self._getter = getter
self._value = None
self._visible = visible
self._format = format
if _type is not None:
self._type = _type
if self._type is None:
if value is not None:
# Infer type if not provided by subclassing
self._type = type(value)
else:
# Bypass type conversion if type not known
self._type = lambda x: x
self._main_layout = layout = QtWidgets.QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
# Give the param a label
self.label = QtWidgets.QLabel()
self.label.setText(name + ":")
layout.addWidget(self.label, 0, 0)
# Give the param an input box
self.input, make_set_button = self._make_input(
value, self._value_change_handler
)
if self._setter is None and value is not None:
self._value = self._type(value)
if self._value is None:
# Highlight that the setter or getter haven't been called yet
self.input.setStyleSheet("QWidget { background-color: #fcd9ca; }")
layout.addWidget(self.input, 0, 1)
# self.set_value(value)
# Give it buttons for setting and getting the value
if self._getter is not None:
self._make_get_button()
if self._setter is not None and make_set_button:
self._make_set_button()
def _make_set_button(self):
self._set_button = QtWidgets.QToolButton()
# Icon list: https://www.pythonguis.com/faq/built-in-qicons-pyqt/
icon = self.style().standardIcon(
QtWidgets.QStyle.StandardPixmap.SP_DialogApplyButton
)
self._set_button.setIcon(icon)
# Using a lambda as the clicked signal always passes False as the first argument
self._set_button.clicked.connect(lambda x: self.set_value())
self._main_layout.addWidget(self._set_button, 0, 3)
def _make_get_button(self):
self._get_button = QtWidgets.QToolButton()
icon = self.style().standardIcon(
QtWidgets.QStyle.StandardPixmap.SP_BrowserReload
)
self._get_button.setIcon(icon)
self._get_button.clicked.connect(lambda x: self.get_value())
self._main_layout.addWidget(self._get_button, 0, 2)
def _value_change_handler(self):
if self._setter is not None:
# Highlight the input box if a setter is set
self.input.setStyleSheet("QWidget { background-color: #fcd9ca; }")
else:
# If there's no setter, we call set_value to set the value from input
self.set_value()
[docs]
def set_value(self, value=None):
"""
Set the value of the param. If a setter is registered, it will be called.
If the setter returns a value, this will become the new value of this param.
If the setter doesn't return a value, the getter will be called if present,
and the value it returns will become the new value of this param.
If the setter doesn't return a value, and there is no getter, the value
provided as an argument will become the new value of this param
:param value: The value this param should be set to (if None, we grab the value from
the param's input box.)
:returns: The new value of the param.
"""
# If a value is not provided, grab one from the input
if value is None:
value = self._input_get_value()
else:
# Otherwise push the given value to the input
value = self._type(value)
self._input_set_value(value)
if self._setter is not None:
# Call setter if it exists. It may return a new value.
new_value = self._setter(value)
if new_value is None:
# If the setter did not return a value, see if there is a getter
if self._getter is not None:
new_value = self._getter()
new_value = self._type(new_value)
else:
# Otherwise the new value is just the value we're setting
new_value = value
# Update the value stored to the new value
self._value = new_value
# Update the input as well
self._input_set_value(new_value)
else:
self._value = value
# Clear the highlight and emit the changed signal
self.input.setStyleSheet("")
self.changed.emit()
return self._value
[docs]
def get_value(self):
"""
Get the value for this param. If a getter is registered, it will be called, and the
returned value becomes the params new value, and is returned by this method.
:returns: Value of the param (retuned by the getter if registered, otherwise the value
currently stored by the param).
"""
if self._getter is not None:
new_value = self._getter()
new_value = self._type(new_value)
self._value = new_value
# Set the value to the input and emit signal if needed
self._input_set_value(new_value)
self.input.setStyleSheet("")
self.changed.emit()
return new_value
else:
return self._value
[docs]
def set_setter(self, piece):
"""
Create a decorator to register a setter for this param. This would be used within
:func:`puzzlepiece.piece.Piece.define_params` as ``@param.set_setter(self)`` decorating
a function.
"""
def decorator(setter):
wrapper = wrap_setter(piece, setter)
self._setter = wrapper
self._make_set_button()
return self
return decorator
[docs]
def set_getter(self, piece):
"""
Create a decorator to register a getter for this param. This would be used within
:func:`puzzlepiece.piece.Piece.define_params` as ``@param.set_getter(self)`` decorating
a function.
"""
def decorator(getter):
wrapper = wrap_getter(piece, getter)
self._getter = wrapper
self._make_get_button()
return self
return decorator
@property
def value(self):
"""
Current, internally stored value.
The getter is not called when this property is accessed.
"""
return self._value
@property
def type(self):
"""
The fixed type of this param. The values set with
:func:`puzzlepiece.param.BaseParam.set_value` will be cast to this type,
and those returned by :func:`puzzlepiece.param.BaseParam.get_value`
will be of this type.
"""
return self._type
@property
def visible(self):
"""
Bool flag indicating whether the param is displayed in the GUI.
"""
return self._visible
def keyPressEvent(self, event):
"""
# Pressing enter triggers the set action.
:meta private:
"""
if (
event.key() == QtCore.Qt.Key.Key_Enter
or event.key() == QtCore.Qt.Key.Key_Return
):
self.set_value()
# Move focus out of the input, so other keyboard shortcuts can be processed
self.setFocus()
else:
super().keyPressEvent(event)
[docs]
class ParamInt(BaseParam):
"""
A param with an integer input field. See the :func:`~puzzlepiece.param.spinbox` decorator below
for how to use this in your Piece.
"""
_type = int
def __init__(
self,
name,
value,
v_min,
v_max,
setter,
getter=None,
visible=True,
v_step=1,
*args,
**kwargs,
):
self._v_min = v_min
self._v_max = v_max
self._v_step = v_step
super().__init__(name, value, setter, getter, visible, *args, **kwargs)
def _make_input(self, value=None, connect=None):
""":meta private:"""
input = QtWidgets.QSpinBox()
input.setMinimum(self._v_min)
input.setMaximum(self._v_max)
input.setSingleStep(self._v_step)
input.setGroupSeparatorShown(True)
if value is not None:
input.setValue(value)
if connect is not None:
input.valueChanged.connect(lambda x: connect())
return input, True
def _input_set_value(self, value):
""":meta private:"""
self.input.blockSignals(True)
self.input.setValue(value)
self.input.blockSignals(False)
def _input_get_value(self):
""":meta private:"""
return self.input.value()
[docs]
class ParamFloat(ParamInt):
"""
A param with a float input field. See the :func:`~puzzlepiece.param.spinbox` decorator below
for how to use this in your Piece.
"""
_type = float
def _make_input(self, value=None, connect=None):
""":meta private:"""
input = QtWidgets.QDoubleSpinBox()
input.setGroupSeparatorShown(True)
input.setMinimum(self._v_min)
input.setMaximum(self._v_max)
input.setSingleStep(self._v_step)
if value is not None:
input.setValue(value)
if connect is not None:
input.valueChanged.connect(connect)
return input, True
class _Slider(QtWidgets.QWidget):
def __init__(self, value, v_min, v_max, v_step, format="{:.2f}"):
self._v_min = v_min
self._v_max = v_max
self._v_step = v_step
self._format = format
super().__init__()
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
self.input = input = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
input.setMinimum(int(np.round(v_min / v_step)))
input.setMaximum(int(np.round(v_max / v_step)))
input.setTickPosition(input.TickPosition.TicksBelow)
layout.addWidget(input)
self.valueChanged = self.input.valueChanged
self.input.valueChanged.connect(self._set_label)
self.input_label = QtWidgets.QLabel()
layout.addWidget(self.input_label)
if value is not None:
input.setValue(int(np.round(value) / v_step))
self._set_label()
def _set_label(self):
value = self.input.value() * self._v_step
self.input_label.setText(self._format.format(value))
def setValue(self, value):
self.input.setValue(int(np.round(value / self._v_step)))
def value(self):
return self.input.value() * self._v_step
[docs]
class ParamSlider(ParamFloat):
"""
A param with a slider, float or int depending on provided value and step.
See the :func:`~puzzlepiece.param.slider` decorator below
for how to use this in your Piece.
"""
def _make_input(self, value=None, connect=None):
if (
isinstance(value, int)
and isinstance(self._v_min, int)
and isinstance(self._v_min, int)
and isinstance(self._v_step, int)
):
self._type = int
format = "{:.2f}" if self._type is float else "{}"
input = _Slider(value, self._v_min, self._v_max, self._v_step, format)
if value is not None:
input.setValue(value)
if connect is not None:
input.valueChanged.connect(connect)
return input, True
[docs]
class ParamText(BaseParam):
"""
A param with a text input field. See the :func:`~puzzlepiece.param.text` decorator below
for how to use this in your Piece.
"""
_type = str
def _make_input(self, value=None, connect=None):
""":meta private:"""
input = QtWidgets.QLineEdit()
if value is not None:
input.setText(value)
if connect is not None:
input.textChanged.connect(connect)
return input, True
def _input_set_value(self, value):
""":meta private:"""
self.input.blockSignals(True)
self.input.setText(value)
self.input.blockSignals(False)
def _input_get_value(self):
""":meta private:"""
return self.input.text()
[docs]
class ParamCheckbox(BaseParam):
"""
A param with a checkbox input field. See the :func:`~puzzlepiece.param.checkbox` decorator below
for how to use this in your Piece.
This param runs :func:`puzzlepiece.param.Param.set_value` automatically when the checkbox
is pressed, so an additional setter button is not displayed.
"""
_type = int
# TODO: handle exceptions better!
def __init__(self, name, value, setter, getter=None, visible=True, *args, **kwargs):
super().__init__(name, value, setter, getter, visible, *args, **kwargs)
def _make_input(self, value=None, connect=None):
""":meta private:"""
input = QtWidgets.QCheckBox()
if value is not None:
input.setChecked(bool(value))
# Handling the input change signal differently here to allow for some error handling
# when the checkbox is clicked
self._connected_click_handler = connect
input.clicked.connect(self._click_handler)
return input, False
def _input_set_value(self, value):
""":meta private:"""
self.input.setChecked(bool(value))
# if self._connected_click_handler is not None:
# self._connected_click_handler()
def _input_get_value(self):
""":meta private:"""
return int(self.input.isChecked())
def _click_handler(self, _):
try:
if self._connected_click_handler is not None:
self._connected_click_handler()
if self._setter is not None:
# If there's a setter, we need to explicitly call set_value here
self.set_value()
except Exception as e:
# Flip back the checkbox if the click resulted in an error
self.input.setChecked(not (self.input.isChecked()))
raise e
class _PartialAccessor:
def __init__(self, param):
self.param = param
def __setitem__(self, key, value):
self.param._value.__setitem__(key, value)
self.param.set_value()
[docs]
class ParamArray(BaseParam):
"""
A param that stores a numpy array. There is no GUI input, the Param simply displays the
dimensions of the array, and indicates when the data has been updated.
The array can be modified through programmatic interaction with setter or getter functions
(for example the array can be obtained from a hardware spectrometer), or treated as a variable
and set using :func:`~puzzlepiece.param.BaseParam.set_value`.
"""
_type = np.asarray
def __init__(
self,
name,
value,
setter=None,
getter=None,
visible=True,
_type=None,
*args,
**kwargs,
):
self._indicator_state = True
self._partial_accessor = _PartialAccessor(self)
super().__init__(
name, value, setter, getter, visible, _type=_type, *args, **kwargs
)
@property
def set_partial(self):
"""
Use this property to set values to slices of the stored numpy array, using
any slicing methods that a numpy array accepts::
puzzle['piece'].params['image'].set_partial[100:200, :] = 128
This will call the param's setter if there's one, and in general
acts like :func:`~puzzlepiece.param.BaseParam.set_value`.
"""
return self._partial_accessor
def _make_input(self, value=None, connect=None):
"""
:meta private:
"""
input = QtWidgets.QLabel()
if value is not None:
input.setText(self._format_array(value))
return input, True
def _input_set_value(self, value):
"""
:meta private:
"""
self.input.setText(self._format_array(value))
def _input_get_value(self):
"""
:meta private:
"""
return self._value
def set_value(self, value=None):
"""
This method overrides :func:`puzzlepiece.param.BaseParam.set_value`.
See there for documentation.
:meta private:
"""
# Small check to account for the label being set twice
# in super().set_value(value) when the
# value argument is None
if value is not None:
self._indicator_state = not self._indicator_state
return super().set_value(value)
def _format_array(self, value):
self._indicator_state = not self._indicator_state
return f"array{value.shape} {'◧' if self._indicator_state else '◨'}"
[docs]
class ParamDropdown(BaseParam):
"""
A param storing a string that also provides a dropdown. The user can edit the text field directly,
and pressing enter will add the current value to the dropdown. A list of values can also be provided
when creating this param, and they will populate the dropdown as soon as the app starts.
The default param value provided here as `value` will either be selected from the dropdown if
in `values`, or inserted into the text field as if a user typed it if not in `values`.
:param values: List of default values available in the dropdown (can be None)
"""
_type = str
def __init__(
self,
name,
value,
values,
setter=None,
getter=None,
visible=True,
*args,
**kwargs,
):
if values is None:
self._values = []
else:
self._values = list(values)
super().__init__(name, value, setter, getter, visible, *args, **kwargs)
def _make_input(self, value=None, connect=None):
""":meta private:"""
input = QtWidgets.QComboBox(editable=True)
# Add the possible values
input.addItems([str(x) for x in self._values])
if value is not None:
value = str(value)
if index := input.findData(value) > -1:
input.setCurrentIndex(index)
else:
input.setCurrentText(value)
if connect is not None:
input.currentTextChanged.connect(connect)
return input, True
def _input_set_value(self, value):
""":meta private:"""
value = str(value)
self.input.blockSignals(True)
if index := self.input.findData(value) > -1:
self.input.setCurrentIndex(index)
else:
self.input.setCurrentText(value)
self.input.blockSignals(False)
def _input_get_value(self):
""":meta private:"""
return self.input.currentText()
[docs]
class ParamProgress(BaseParam):
"""
A param with a progress bar. See the :func:`~puzzlepiece.param.progress` decorator below
for how to use this in your Piece.
"""
_type = float
def _make_input(self, value=None, connect=None):
""":meta private:"""
input = QtWidgets.QProgressBar()
input.setMinimum(0)
input.setMaximum(1000)
if value is not None:
input.setValue(value)
return input, True
def _input_set_value(self, value):
""":meta private:"""
if value < 0:
self.input.setMaximum(0)
else:
self.input.setMaximum(1000)
self.input.setValue(int(value * 1000))
def _input_get_value(self):
""":meta private:"""
return self.input.value()
[docs]
def iter(self, iterable):
"""
Iterable wrapper that automatically updates the progress bar while the provided
iterable is iterated over. This is quite similar to how the console progress bar
``tqdm`` works: https://tqdm.github.io/
For example::
for i in piece.params['progress'].iter(range(10)):
print(i)
# this will automatically update the progress bar as the iteration goes on
puzzle.process_events() # you may need to refresh the Puzzle to display changes
:param iterable: an iterable to wrap
:rtype: iterable
"""
if hasattr(iterable, "__len__"):
length = len(iterable)
else:
length = -1
for i, value in enumerate(iterable):
self.set_value(i / length)
yield value
self.set_value(1)
def wrap_setter(piece, setter):
"""
We wrap the setter function such that it can be called without passing
a reference to the Piece as the first argument
:meta private:
"""
if setter is not None:
@wraps(setter)
def wrapper(value):
return setter(piece, value)
else:
wrapper = None
return wrapper
def wrap_getter(piece, getter):
"""
We wrap the getter function such that it can be called without passing
a reference to the Piece as the first argument
:meta private:
"""
if getter is not None:
@wraps(getter)
def wrapper():
return getter(piece)
else:
wrapper = None
return wrapper
# The decorator syntax in Python is a little confusing
[docs]
def base_param(piece, name, value, visible=True, format="{}"):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.BaseParam` in a Piece's
:func:`~puzzlepiece.piece.Piece.define_params` method with a given **setter**.
This will simply display the current value with no option to edit it.
To register a param and mark a function as its setter, do this::
@puzzlepiece.param.base_param(self, 'param_name', 0)
def param_setter(self, value):
print(value)
To register a param without a setter, call this function to get a decorator, and then pass ``None`` to that to indicate
that a setter doesn't exist::
puzzlepiece.param.base_param(self, 'param_name', 0)(None)
Some of the decorators here decorate getters, and some decorate setters, depending on what is the more sensible default
-- a :func:`~puzzlepiece.param.readout` decorates a getter, as it is meant to display obtained values, and a
:func:`~puzzlepiece.param.spinbox` decorates a setter, as it's meant to be an easy way to input and set values. If you
need to also register the other function, use the :func:`puzzlepiece.param.BaseParam.set_getter`
and :func:`puzzlepiece.param.BaseParam.set_setter` decorators::
@puzzlepiece.param.base_param(self, 'position', 0)
def position(self, value):
self.sdk.set_position(value)
@position.set_getter(self)
def position(self):
return self.sdk.get_position()
:param piece: The :class:`~puzzle.piece.Piece` this param should be registered with. Usually `self`, as this method should
be called from within :func:`puzzlepiece.piece.Piece.define_params`
:param name: a unique (per Piece) name for the param
:param value: default display value for the param. If there is a setter, the setter will not be called, and the stored value
will remain None until the param is explicitly set (either through :func:`~puzzlepiece.param.BaseParam.set_value`, or
pressing the set button.)
:param visible: bool flag, determined if a GUI component will be shown for this param.
:param format: Default format for displaying the param if a custom input is not defined, and in
:func:`puzzplepiece.parse.format`. For example `{:.2f}`, see https://pyformat.info/ for further details.
:returns: A decorator that can be applied to a setter function. The decorator returns the param object when called.
"""
# The base_param function *creates and returns a decorator*,
# it's just the way to enable passing parameters when decorating something
def decorator(setter):
wrapper = wrap_setter(piece, setter)
# Register the param with the Piece
piece.params[name] = BaseParam(
name, value, setter=wrapper, getter=None, visible=visible, format=format
)
# Return the newly created Param
return piece.params[name]
return decorator
[docs]
def readout(piece, name, visible=True, format="{}", _type=None):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.BaseParam` in a Piece's
:func:`~puzzlepiece.piece.Piece.define_params` method with a given **getter**.
This will simply display the current value with no option to edit it.
See :func:`~puzzlepiece.param.base_param` for more details.
"""
def decorator(getter):
wrapper = wrap_getter(piece, getter)
piece.params[name] = BaseParam(
name,
None,
setter=None,
getter=wrapper,
visible=visible,
format=format,
_type=_type,
)
return piece.params[name]
return decorator
[docs]
def spinbox(piece, name, value, v_min=-1e9, v_max=1e9, visible=True, v_step=1):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.ParamInt` or :class:`~puzzlepiece.param.ParamFloat`
in a Piece's :func:`~puzzlepiece.piece.Piece.define_params` method with a given **setter**.
The type of param registered depends on whether the ``value`` given is an int of a float.
This will display a Qt spinbox - a numeric input box.
See :func:`~puzzlepiece.param.base_param` for more details.
:param v_min: The minimum value accepted by the spinbox.
:param v_max: The maximum value accepted by the spinbox.
:param v_step: The step change when spinbox buttons are used.
"""
def decorator(setter):
wrapper = wrap_setter(piece, setter)
if isinstance(value, int):
piece.params[name] = ParamInt(
name,
value,
int(v_min),
int(v_max),
setter=wrapper,
getter=None,
visible=visible,
v_step=int(v_step),
)
else:
piece.params[name] = ParamFloat(
name, value, v_min, v_max, wrapper, None, visible, v_step
)
return piece.params[name]
return decorator
[docs]
def slider(piece, name, value, v_min=0, v_max=1, visible=True, v_step=0.05):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.ParamSlider`
in a Piece's :func:`~puzzlepiece.piece.Piece.define_params` method with a given **setter**.
This will display a slider with the specified properties and a label.
It will be an int or a float slider depending on the type of the values provided here.
See :func:`~puzzlepiece.param.base_param` for more details.
"""
def decorator(setter):
wrapper = wrap_setter(piece, setter)
piece.params[name] = ParamSlider(
name, value, v_min, v_max, wrapper, None, visible, v_step
)
return piece.params[name]
return decorator
[docs]
def text(piece, name, value, visible=True):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.ParamText`
in a Piece's :func:`~puzzlepiece.piece.Piece.define_params` method with a given **setter**.
This will display a single line textbox.
See :func:`~puzzlepiece.param.base_param` for more details.
"""
def decorator(setter):
wrapper = wrap_setter(piece, setter)
piece.params[name] = ParamText(name, value, wrapper, None, visible)
return piece.params[name]
return decorator
[docs]
def checkbox(piece, name, value, visible=True):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.ParamCheckbox`
in a Piece's :func:`~puzzlepiece.piece.Piece.define_params` method with a given **setter**.
This will display a checkbox as the input, and the param takes the values 0 and 1 for
unchecked and checked states.
This param runs puzzlepiece.param.Param.set_value() automatically when the checkbox is pressed,
so an additional setter button is not displayed.
See :func:`~puzzlepiece.param.base_param` for more details.
"""
def decorator(setter):
wrapper = wrap_setter(piece, setter)
piece.params[name] = ParamCheckbox(name, value, wrapper, None, visible)
return piece.params[name]
return decorator
[docs]
def array(piece, name, visible=True):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.ParamArray`
in a Piece's :func:`~puzzlepiece.piece.Piece.define_params` method with a given **getter**.
This will display the shape of the stored array with no option to edit it and an indicator
showing when the value changes.
See :func:`~puzzlepiece.param.base_param` for more details.
"""
def decorator(getter):
wrapper = wrap_getter(piece, getter)
piece.params[name] = ParamArray(
name, None, setter=None, getter=wrapper, visible=visible
)
return piece.params[name]
return decorator
[docs]
def dropdown(piece, name, value, visible=True):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.ParamDropdown`
in a Piece's :func:`~puzzlepiece.piece.Piece.define_params`.
It should decorate a function that returns a list of default values to populate the dropdown,
for example::
@puzzlepiece.param.dropdown(self, 'param_name', '')
def param_values(self, value):
return self.sdk.discover_devices()
It can also be used with a set list of defaults, or with no defaults at all::
puzzlepiece.param.dropdown(self, 'param_name', 'one')(['one', 'two', 'three'])
puzzlepiece.param.dropdown(self, 'param_name', 'four')(None)
Setters and getters can then be added using :func:`puzzlepiece.param.BaseParam.set_getter`
and :func:`puzzlepiece.param.BaseParam.set_setter` decorators::
@puzzlepiece.param.dropdown(self, 'serial_number', '')
def serial_number(self, value):
return self.sdk.discover_devices()
@serial_number.set_getter(self)
def serial_number(self):
return self.sdk.get_serial()
@serial_number.set_setter(self)
def serial_number(self, value):
return self.sdk.set_serial(value)
The returned param displays a dropdown and stores a string. The user can edit the dropdown's
text field directly, and pressing enter will add the current value to the dropdown.
The default param value provided here as `value` will either be selected from the dropdown if
available, or inserted into the text field as if a user typed it otherwise.
"""
def decorator(values):
if callable(values):
# `values` can be a function that returns a list of values
values = values(piece)
piece.params[name] = ParamDropdown(name, value, values, None, None, visible)
return piece.params[name]
return decorator
[docs]
def progress(piece, name, visible=True):
"""
A decorator generator for registering a :class:`~puzzlepiece.param.ParamProgress` in a Piece's
:func:`~puzzlepiece.piece.Piece.define_params` method with a given **getter**.
This will display the current progress value on a scale of 0 to 1 with no option to edit it.
See :func:`~puzzlepiece.param.base_param` for more details.
"""
def decorator(getter):
wrapper = wrap_getter(piece, getter)
piece.params[name] = ParamProgress(
name, None, setter=None, getter=wrapper, visible=visible
)
return piece.params[name]
return decorator