Skip to content

Commit

Permalink
Array and Dropdown params
Browse files Browse the repository at this point in the history
setup now happens ahead of param defintion
so that the dropdown population function can use SDKs to find devices for example
  • Loading branch information
jdranczewski committed Nov 27, 2023
1 parent 35889d4 commit d6904a2
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 5 deletions.
192 changes: 191 additions & 1 deletion puzzlepiece/param.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pyqtgraph.Qt import QtWidgets, QtCore
from functools import wraps
import numpy as np

class BaseParam(QtWidgets.QWidget):
"""
Expand Down Expand Up @@ -129,10 +130,10 @@ def set_value(self, value=None):
self._value = new_value
# Update the input as well, clearing the highlight
self._input_set_value(new_value)
self.input.setStyleSheet("")
else:
self._value = value

self.input.setStyleSheet("")
self.changed.emit()

def get_value(self):
Expand All @@ -157,6 +158,19 @@ def get_value(self):
else:
return self._value

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

def set_getter(self, piece):
"""
Create a decorator to register a getter for this param. This would be used within
Expand Down Expand Up @@ -214,6 +228,16 @@ def _input_get_value(self):
"""
return self._type(self.input.text())

@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):
"""
Expand Down Expand Up @@ -360,7 +384,96 @@ def _click_handler(self, _):
# Flip back the checkbox if the click resulted in an error
self.input.setChecked(not(self.input.isChecked()))
raise e


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 programmatically by providing setters or getters, or using
:func:`~puzzlepiece.param.BaseParam.set_value`.
"""
_type = np.asarray

def __init__(self, name, value, setter=None, getter=None, visible=True, format='{}', _type=None, *args, **kwargs):
self._indicator_state = True
super().__init__(name, value, setter, getter, visible, format, _type, *args, **kwargs)

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 _format_array(self, value):
self._indicator_state = not self._indicator_state
return f"array{value.shape} {'◧' if self._indicator_state else '◨'}"


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()

def wrap_setter(piece, setter):
"""
Expand Down Expand Up @@ -412,6 +525,20 @@ def param_setter(self, value):
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
Expand Down Expand Up @@ -508,4 +635,67 @@ def decorator(setter):
wrapper = wrap_setter(piece, setter)
piece.params[name] = ParamCheckbox(name, value, wrapper, None, visible)
return piece.params[name]
return decorator

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

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
7 changes: 4 additions & 3 deletions puzzlepiece/piece.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def __init__(self, puzzle, custom_horizontal=False, *args, **kwargs):
#: dict: A dictionary of this Piece's actions (see :class:`~puzzlepiece.action.Action`)
self.actions = {}
self.shortcuts = {}

if not self.puzzle.debug:
self.setup()

self.define_params()
self.define_readouts()
self.define_actions()
Expand All @@ -49,9 +53,6 @@ def __init__(self, puzzle, custom_horizontal=False, *args, **kwargs):
if custom_layout is None or custom_horizontal:
control_layout.addStretch()

if not self.puzzle.debug:
self.setup()

def param_layout(self, wrap=1):
"""
Genereates a `QGridLayout` for the params. Override to set a different wrapping.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "puzzlepiece"
version = "0.3.0"
version = "0.4.0"
authors = [
{ name="Jakub Dranczewski", email="jakub.dranczewski@gmail.com" },
]
Expand Down

0 comments on commit d6904a2

Please sign in to comment.