1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
|
Use ``ParameterForm`` to easily create Orange widget forms
----------------------------------------------------------
Often, an Orange widget boils down to a form to specify inputs for its associated Ewoks task.
For this common use case, `ewoksorange` provides a ``ParameterForm`` class. Once instantiated, it can generate graphical input elements based on the given input name and types.
For example, let's take a simple Ewoks task ``SumList`` and say we want to write an Orange widget that will allow the user to specify the inputs of this task.
.. code:: python
from ewokscore.task import Task
class SumList(
Task,
input_names=["list"],
optional_input_names=["delay"],
output_names=["sum"],
):
"""Add items from a list"""
def run(self):
if self.inputs.list is None:
raise ValueError("list should be provided")
if self.inputs.delay:
delay = self.inputs.delay
else:
delay = 0
sum_ = 0
for i_elmt, elmt in enumerate(self.inputs.list):
sum_ += elmt
_sleep(delay)
self.outputs.sum = sum_
This task takes a ``list`` as input and iterates over it to sum its elements with an optional ``delay``. We then want the Orange widget to provide us two GUI elements: one textbox to specify the list, and a numeric spinbox to specify the delay.
The Orange widget will then look like this:
.. code:: python
import json
from ewoksorange.bindings import OWEwoksWidgetNoThread
from ewoksorange.gui.parameterform import ParameterForm
from ewokscore.tests.examples.tasks.sumlist import SumList
class OWSumList(
OWEwoksWidgetNoThread,
ewokstaskclass=SumList,
):
def __init__(self):
super().__init__()
self._parameter_form = ParameterForm(parent=self.controlArea)
self._parameter_form.addParameter(
"delay",
label="Delay for each sum iteration",
value_for_type=0,
value_change_callback=self._inputs_changed
)
self._parameter_form.addParameter(
"list",
label="List of elements to sum",
value_for_type="",
serialize=json.dumps,
deserialize=json.loads,
value_change_callback=self._inputs_changed
)
self._update_parameter_values()
def _inputs_changed(self):
new_values = self._parameter_form.get_parameter_values()
self.update_default_inputs(**new_values)
def _update_parameter_values(self):
new_values = self._parameter_form.get_parameter_values()
self.update_default_inputs(**new_values)
.. note::
The full example can be found in `src/orangecontrib/ewokstest/sumlist_parameter_form.py`
There is a lot to unpack so let's do this step by step:
------
.. code:: python
self._parameter_form = ParameterForm(parent=self.controlArea)
This line creates the ``ParameterForm`` in the ``controlArea`` of the widget. The ``controlArea`` (as opposed to the ``mainArea``) is the widget area where control elements (e.g. buttons) are located so it makes sense to put our form there.
------
.. code:: python
self._parameter_form.addParameter(
"delay",
label="Delay for each sum iteration",
value_for_type=0,
value_change_callback=self._inputs_changed
)
This part calls ``addParameter`` a first time to generate the first element of our form.
``addParameter`` only has one mandatory argument: the name (``delay``) that is used to uniquely identify the parameter.
The other specified arguments are:
* ``label``: A string that will be displayed next to the GUI element.
* ``value_for_type``: A Python value whose type will be used to determine the GUI element to show. In this case, we give a ``number`` (``0`` but it could be any number) so that ``ParameterForm`` shows a numerical spinbox. See the end of the page for a list of all possible values.
* ``value_change_callback``: The function that will be called when the value is changed. We will come back to this later.
------
.. code:: python
self._parameter_form.addParameter(
"list",
label="List of elements to sum",
value_for_type="",
serialize=json.dumps,
deserialize=json.loads,
value_change_callback=self._inputs_changed
)
This part calls ``addParameter`` a second time to generate the second element of our form used to specify the ``list`` parameter.
We already saw the ``label`` and ``value_change_callback`` arguments.
Since on the GUI side, a user can only input numbers or strings, this parameter will be a ``string`` (hence the ``string`` in ``value_for_type`` so that the GUI will have a textbox).
However, we can apply a transformation to the value when retrieving it from the GUI: this is the role of the function given as ``deserialize`` argument. By doing ``json.loads`` on the string representing a ``list``, we can get a list as parameter value instead not a string. The ``serialize`` is the inverse operation (when setting the value from the widget to the GUI).
------
.. code:: python
def _inputs_changed(self):
new_values = self._parameter_form.get_parameter_values()
self.update_default_inputs(**new_values)
This is the function that will be called when the ``ParameterForm`` values change. ``get_parameter_values`` allows us to retrieve the dictionnary of parameter values (the keys being the ``name`` specified in ``addParameter``).
For these values to be used as inputs of the Ewoks task, we must update the default inputs of the task. This is done by ``update_default_inputs`` that takes arguments ``key=value``, the keys being the names of the Ewoks task inputs. Since the names of the parameters and of the Ewoks task inputs are the same (``list`` and ``delay``), we can directly use the ``new_values`` dictionnary coming from the ``ParameterForm``.
.. warning::
The parameter values are set as **default inputs**. It means they will be overwritten by **dynamic inputs** that come from upstream connections or by execution inputs.
------
.. code:: python
def _update_parameter_values(self):
initial_values = self.get_default_inputs()
self._parameter_form.set_parameter_values(**initial_values)
A saved workflow can hold default values for each task. By calling this function when creating the Orange widget, we ensure that these initial default values are propagated to the parameter form so that it is initialized with the right values.
Possible `value_for_type` and their associated GUI elements
===========================================================
.. list-table::
:header-rows: 1
* - Type
- `value_for_type` example
- GUI element
- Notes
* - `bool`
- `True`
- Checkbox
-
* - `Number`
- `0`
- Spinbox
- The spinbox will allow decimals only if a `float` is supplied
* - `Sequence`
- `['Choice1', 'Choice2', 'Choice3']`
- Combobox
- The choices of the combobox must be specified in the given sequence (an empty list will generate a combobox with no choices).
* - `str`
- `""`
- Textbox
- Can be used as input for list and dict if `json` is used for the serialization/deserialization.
.. note::
In case the textbox is meant for a path (to a file/directory/HDF5 entity), the `select` argument can be supplied in conjunction of the string `value_for_type`.
Specifying the `select` argument will make a button appear next to the textbox that opens a file browser. Selecting an entity in this file browser will automatically fill the checkbox with the entity path.
The possible selectable entities depend on the value of the `select` argument: `file`, `newfile`, `directory`, `h5dataset`, `h5group`, `files`, `newfiles`, `directories`, `h5datasets` or `h5groups`.
|