Source code for ewoksorange.gui.widgets.parameter_form
try:
from enum import StrEnum
except ImportError:
from backports.strenum import StrEnum
import logging
import numbers
import os
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Union
from AnyQt import QtCore
from AnyQt import QtWidgets
from ewokscore import missing_data
from silx.gui.dialog.DataFileDialog import DataFileDialog
from ..qt_utils.signals import block_signals
_logger = logging.getLogger(__name__)
ParameterValueType = Any
WidgetValueType = Union[str, numbers.Number, bool, missing_data.MissingData]
[docs]
class SelectMode(StrEnum):
FILE = "file"
NEW_FILE = "newfile"
FILES = "files"
NEW_FILES = "newfiles"
DIRECTORY = "directory"
DIRECTORIES = "directories"
H5_DATASET = "h5dataset"
H5_DATASETS = "h5datasets"
H5_GROUP = "h5group"
H5_GROUPS = "h5groups"
def _default_serialize(value: ParameterValueType) -> WidgetValueType:
return value
def _default_deserialize(value: WidgetValueType) -> ParameterValueType:
return value
[docs]
class ParameterForm(QtWidgets.QWidget):
def __init__(self, *args, margin=0, spacing=4, **kwargs):
super().__init__(*args, **kwargs)
self._init_ui(margin=margin, spacing=spacing)
self._fields = dict()
def _init_ui(self, margin=0, spacing=4):
self._init_parent_ui()
layout = QtWidgets.QGridLayout()
layout.setContentsMargins(margin, margin, margin, margin)
layout.setSpacing(spacing)
self.setLayout(layout)
def _init_parent_ui(self):
parent = self.parent()
if parent is None:
return
layout = parent.layout()
if layout is None:
layout = QtWidgets.QVBoxLayout()
parent.setLayout(layout)
layout.addWidget(self)
[docs]
def addParameter(
self,
name: str,
value: ParameterValueType = missing_data.MISSING_DATA,
value_for_type: Any = "",
value_change_callback: Optional[Callable] = None,
label: Optional[str] = None,
readonly: Optional[bool] = None,
enabled: Optional[bool] = None,
select: Union[SelectMode, str, None] = None,
select_label: str = "...",
checked: Optional[bool] = None,
checkbox_label: str = "checked",
serialize: Callable[[ParameterValueType], WidgetValueType] = _default_serialize,
deserialize: Callable[
[WidgetValueType], ParameterValueType
] = _default_deserialize,
bool_label: str = "",
):
"""Add one row to the form.
Each row contains:
- a label widget (left column)
- a value widget (centre column): type determined by *value_for_type*
- an optional select button (right column): opened when *select* is set
- an optional checkbox (far-right column): present when *checked* is set
Parameters
----------
name:
Identifier of the row
value:
Initial value displayed in the widget. Defaults to MISSING_DATA.
value_for_type:
Determines the widget type:
- ``str`` → QLineEdit
- ``bool`` → QCheckBox
- ``numbers.Integral`` → QSpinBox
- ``numbers.Real`` → QDoubleSpinBox
- ``Sequence`` → QComboBox (items are the choices)
value_change_callback:
Called with no arguments whenever the user edits the value.
When provided, *readonly* defaults to False; otherwise True.
label:
Text shown in the label widget. Defaults to *name*.
readonly:
When True the value widget is not editable.
Default: True when no *value_change_callback*, False otherwise.
enabled:
When False all widgets in the row are greyed out. Default: True.
select:
Adds a ``"..."`` button that opens a dialog. See :class:`SelectMode`.
Only valid when *value_for_type* is a ``str``.
select_label:
Text shown on the select button. Default: ``"..."``.
checked:
When set, adds a checkbox in the far-right column initialised to
this value. Its meaning is application-defined.
checkbox_label:
Text shown next to the optional checkbox. Default: ``"checked"``.
serialize:
Converts the parameter value to a value the widget can display
(str, number, or bool). Identity by default.
deserialize:
Converts the widget's internal value back to a parameter value.
Identity by default. Should raise on invalid input so the form
can fall back to MISSING_DATA.
bool_label:
Text shown inside the QCheckBox when *value_for_type* is a bool.
Default: ``""``.
"""
if not label:
label = name
if select is not None:
select = SelectMode(select)
has_callback = bool(value_change_callback)
if readonly is None:
readonly = not has_callback
if enabled is None:
enabled = True
_logger.debug(
"Parameter form: initialize parameter %r (readonly = %s, enabled = %s, type = %s, missing = %s)",
name,
readonly,
enabled,
type(value_for_type),
missing_data.is_missing_data(value),
)
label_widget = QtWidgets.QLabel(label)
value_widget = None
select_widget = None
check_widget = None
connections = list()
if isinstance(value_for_type, str):
value_widget = QtWidgets.QLineEdit()
if value_change_callback:
connections.append(
(value_widget.editingFinished.connect, value_change_callback)
)
if select == SelectMode.FILE:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_file(
name,
must_exist=True,
value_change_callback=value_change_callback,
),
)
)
elif select == SelectMode.NEW_FILE:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_file(
name,
must_exist=False,
value_change_callback=value_change_callback,
),
)
)
elif select == SelectMode.DIRECTORY:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_directory(
name, value_change_callback=value_change_callback
),
)
)
elif select == SelectMode.H5_DATASET:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_h5dataset(
name, value_change_callback=value_change_callback
),
)
)
elif select == SelectMode.H5_GROUP:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_h5group(
name, value_change_callback=value_change_callback
),
)
)
elif select == SelectMode.FILES:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_file(
name,
must_exist=True,
append=True,
value_change_callback=value_change_callback,
),
)
)
elif select == SelectMode.NEW_FILES:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_file(
name,
must_exist=False,
append=True,
value_change_callback=value_change_callback,
),
)
)
elif select == SelectMode.DIRECTORIES:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_directory(
name,
append=True,
value_change_callback=value_change_callback,
),
)
)
elif select == SelectMode.H5_DATASETS:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_h5dataset(
name,
append=True,
value_change_callback=value_change_callback,
),
)
)
elif select == SelectMode.H5_GROUPS:
select_widget = QtWidgets.QPushButton(select_label)
connections.append(
(
select_widget.pressed.connect,
lambda: self._select_h5group(
name,
append=True,
value_change_callback=value_change_callback,
),
)
)
else:
select_widget = None
elif isinstance(value_for_type, bool):
value_widget = QtWidgets.QCheckBox(bool_label)
if value_change_callback:
connections.append(
(value_widget.stateChanged.connect, value_change_callback)
)
elif isinstance(value_for_type, numbers.Number):
if isinstance(value_for_type, numbers.Integral):
value_widget = QtWidgets.QSpinBox()
value_widget.setRange(-(2**31), 2**31 - 1)
else:
value_widget = QtWidgets.QDoubleSpinBox()
value_widget.setRange(-(2**52), 2**52 - 1)
if value_change_callback:
connections.append(
(value_widget.editingFinished.connect, value_change_callback)
)
elif isinstance(value_for_type, Sequence):
value_widget = QtWidgets.QComboBox()
value_widget.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
value_widget.addItem("<missing>", missing_data.MISSING_DATA)
for data in value_for_type:
data = serialize(data)
value_widget.addItem(str(data), data)
if value_change_callback:
connections.append(
(value_widget.currentIndexChanged.connect, value_change_callback)
)
else:
raise TypeError(
f"Parameter '{name}' with type '{type(value)}' does not have a Qt widget"
)
if checked is not None:
check_widget = QtWidgets.QCheckBox(checkbox_label)
check_widget.setChecked(checked)
if value_change_callback:
connections.append(
(check_widget.stateChanged.connect, value_change_callback)
)
policy = QtWidgets.QSizePolicy.Expanding
value_widget.setSizePolicy(policy, policy)
grid = self.layout()
row = grid.rowCount()
grid.addWidget(label_widget, row, 0)
if value_widget:
grid.addWidget(value_widget, row, 1)
if select_widget:
grid.addWidget(select_widget, row, 2)
if check_widget:
grid.addWidget(check_widget, row, 3)
self._fields[name] = {
"row": row,
"deserialize": deserialize,
"serialize": serialize,
}
self.set_parameter_value(name, value)
self.set_parameter_readonly(name, readonly)
self.set_parameter_enabled(name, enabled)
for connect, func in connections:
connect(func)
[docs]
def addStretch(self):
"""Append a stretchable blank row to the form"""
grid = self.layout()
grid.setRowStretch(grid.rowCount(), 1)
def _get_widget(self, name: str, col: int) -> QtWidgets.QWidget:
if name not in self._fields:
return None
row = self._fields[name]["row"]
item = self.layout().itemAtPosition(row, col)
if item is None:
return None
return item.widget()
def _get_label_widget(self, name: str) -> QtWidgets.QWidget:
return self._get_widget(name, 0)
def _get_value_widget(self, name: str) -> QtWidgets.QWidget:
return self._get_widget(name, 1)
def _get_select_widget(self, name: str) -> QtWidgets.QWidget:
return self._get_widget(name, 2)
def _get_check_widget(self, name: str) -> QtWidgets.QWidget:
return self._get_widget(name, 3)
[docs]
def get_parameter_value(self, name: str):
w = self._get_value_widget(name)
if w is None:
return missing_data.MISSING_DATA
if isinstance(w, QtWidgets.QLineEdit):
wvalue = w.text()
elif isinstance(w, QtWidgets.QCheckBox):
wvalue = w.isChecked()
elif isinstance(w, QtWidgets.QComboBox):
wvalue = w.currentData()
# TODO: check what it returns when index is -1
else:
wvalue = w.value()
fdict = self._fields[name]
changed = wvalue != fdict["last_widget_value"]
if changed:
fdict["last_widget_value"] = wvalue
elif fdict["null"]:
return missing_data.MISSING_DATA
deserialize = fdict["deserialize"]
try:
value = deserialize(wvalue)
except Exception:
value = missing_data.MISSING_DATA
fdict["null"] = True
else:
fdict["null"] = missing_data.is_missing_data(value)
return value
[docs]
def set_parameter_value(self, name: str, value: ParameterValueType):
"""When the value cannot be serialized or cannot be set to the widget,
the parameter value will set to MISSING_DATA.
"""
w = self._get_value_widget(name)
if w is None:
return
fdict = self._fields[name]
serialize = fdict["serialize"]
if missing_data.is_missing_data(value):
set_value = missing_data.MISSING_DATA
else:
try:
set_value = serialize(value)
except Exception:
set_value = missing_data.MISSING_DATA
if isinstance(w, QtWidgets.QLineEdit):
_logger.debug("Parameter form: set string parameter %r = %r", name, value)
try:
with block_signals(w):
w.setText(set_value)
except TypeError:
with block_signals(w):
w.setText("")
fdict["null"] = True
fdict["last_widget_value"] = ""
else:
fdict["null"] = False
fdict["last_widget_value"] = set_value
elif isinstance(w, QtWidgets.QCheckBox):
_logger.debug("Parameter form: set boolean parameter %r = %r", name, value)
try:
with block_signals(w):
w.setChecked(set_value)
except TypeError:
with block_signals(w):
w.setChecked(False)
fdict["null"] = True
fdict["last_widget_value"] = False
else:
fdict["null"] = False
fdict["last_widget_value"] = set_value
elif isinstance(w, QtWidgets.QComboBox):
_logger.debug(
"Parameter form: select choice parameter %r = %r", name, value
)
idx = w.findData(set_value)
if idx == -1:
with block_signals(w):
w.setCurrentIndex(0)
fdict["null"] = True
fdict["last_widget_value"] = missing_data.MISSING_DATA
else:
with block_signals(w):
w.setCurrentIndex(idx)
fdict["null"] = False
fdict["last_widget_value"] = set_value
else:
_logger.debug(
"Parameter form: set numerical parameter %r = %r", name, value
)
try:
with block_signals(w):
w.setValue(set_value)
except TypeError:
with block_signals(w):
w.setValue(0)
fdict["null"] = True
fdict["last_widget_value"] = 0
else:
fdict["null"] = False
fdict["last_widget_value"] = set_value
[docs]
def get_parameter_readonly(self, name: str) -> Optional[bool]:
w = self._get_value_widget(name)
if w is not None:
if isinstance(w, QtWidgets.QCheckBox):
return w.isEnabled()
elif isinstance(w, QtWidgets.QComboBox):
return w.isEnabled()
else:
return w.isReadOnly()
[docs]
def set_parameter_readonly(self, name: str, value: bool) -> None:
w = self._get_value_widget(name)
if w is not None:
if isinstance(w, QtWidgets.QCheckBox):
return w.setEnabled(value)
elif isinstance(w, QtWidgets.QComboBox):
return w.setEnabled(value)
else:
return w.setReadOnly(value)
[docs]
def get_parameter_enabled(self, name: str) -> Optional[bool]:
w = self._get_value_widget(name)
if w is not None:
return w.isEnabled()
[docs]
def set_parameter_enabled(self, name: str, value: bool) -> None:
w = self._get_value_widget(name)
if w is not None:
w.setEnabled(value)
[docs]
def get_parameter_checked(self, name: str) -> Optional[bool]:
w = self._get_check_widget(name)
if w is not None:
return w.isChecked()
[docs]
def set_parameter_checked(self, name: str, value: bool) -> None:
w = self._get_check_widget(name)
if w is not None:
w.setChecked(value)
[docs]
def get_parameter_values(self) -> Dict[str, ParameterValueType]:
return {name: self.get_parameter_value(name) for name in self._fields}
[docs]
def set_parameter_values(self, params: Dict[str, ParameterValueType]) -> None:
for name, value in params.items():
self.set_parameter_value(name, value)
[docs]
def get_parameters_readonly(self) -> Dict[str, bool]:
return {name: self.get_parameter_readonly(name) for name in self._fields}
[docs]
def set_parameters_readonly(self, params: Dict[str, bool]) -> None:
for name, value in params.items():
self.set_parameter_readonly(name, value)
[docs]
def get_parameters_checked(self) -> Dict[str, bool]:
result = dict()
for name in self._fields:
checked = self.get_parameter_checked(name)
if checked is None:
continue
result[name] = checked
return result
[docs]
def set_parameters_checked(self, params: Dict[str, bool]) -> None:
for name, value in params.items():
self.set_parameter_checked(name, value)
[docs]
def get_parameters_enabled(self) -> Dict[str, bool]:
return {name: self.get_parameter_enabled(name) for name in self._fields}
[docs]
def set_parameters_enabled(self, params: Dict[str, bool]) -> None:
for name, value in params.items():
self.set_parameter_enabled(name, value)
def _select_file(
self,
name: str,
must_exist: bool = True,
append: bool = False,
value_change_callback=Optional[Callable[[], None]],
) -> None:
if append:
filename = self._list_value_first(name)
else:
filename = self.get_parameter_value(name)
dialog = QtWidgets.QFileDialog(self)
if must_exist:
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
else:
dialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
if filename:
dialog.setDirectory(os.path.dirname(filename))
if not dialog.exec():
dialog.close()
return
value = dialog.selectedFiles()[0]
if append:
value = self._list_value_append(name, value)
self.set_parameter_value(name, value)
if value_change_callback:
value_change_callback()
def _select_h5dataset(
self,
name: str,
append: bool = False,
value_change_callback=Optional[Callable[[], None]],
) -> None:
if append:
url = self._list_value_first(name)
else:
url = self.get_parameter_value(name)
dialog = DataFileDialog(self)
dialog.setFilterMode(DataFileDialog.FilterMode.ExistingDataset)
if url:
dialog.selectUrl(url)
if not dialog.exec():
dialog.close()
return
value = dialog.selectedUrl()
if value:
value = value.replace("?/", "?path=/")
if append:
value = self._list_value_append(name, value)
self.set_parameter_value(name, value)
if value_change_callback:
value_change_callback()
def _select_h5group(
self,
name: str,
append: bool = False,
value_change_callback=Optional[Callable[[], None]],
) -> None:
if append:
url = self._list_value_first(name)
else:
url = self.get_parameter_value(name)
dialog = DataFileDialog(self)
dialog.setFilterMode(DataFileDialog.FilterMode.ExistingGroup)
if url:
dialog.selectUrl(url)
if not dialog.exec():
dialog.close()
return
value = dialog.selectedUrl()
if value:
value = value.replace("?/", "?path=/")
if append:
value = self._list_value_append(name, value)
self.set_parameter_value(name, value)
if value_change_callback:
value_change_callback()
def _select_directory(
self,
name: str,
append: bool = False,
value_change_callback=Optional[Callable[[], None]],
) -> None:
if append:
directory = self._list_value_first(name)
else:
directory = self.get_parameter_value(name)
dialog = QtWidgets.QFileDialog(self)
dialog.setFileMode(QtWidgets.QFileDialog.Directory)
dialog.setOption(QtWidgets.QFileDialog.ShowDirsOnly)
if directory:
dialog.setDirectory(directory)
if not dialog.exec():
dialog.close()
return
value = dialog.selectedFiles()[0]
if append:
value = self._list_value_append(name, value)
self.set_parameter_value(name, value)
if value_change_callback:
value_change_callback()
def _list_value_append(self, name: str, value: str) -> List[str]:
lst = self.get_parameter_value(name)
if not lst:
lst = [value]
elif isinstance(lst, str):
lst = [lst, value]
elif isinstance(lst, list):
lst.append(value)
else:
raise TypeError(value)
return lst
def _list_value_first(self, name: str) -> Optional[str]:
lst = self.get_parameter_value(name)
if not lst:
return None
elif isinstance(lst, str):
return lst
elif isinstance(lst, list):
return lst[-1]
else:
raise TypeError(lst)
[docs]
def keyPressEvent(self, event):
key = event.key()
if key == QtCore.Qt.Key_Enter or key == QtCore.Qt.Key_Return:
# TODO: Orange3 causes a button in focus to be pressed due to this event.
return
super().keyPressEvent(event)