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 has_parameter(self, name: str): w = self._get_value_widget(name) return w is not None
[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_names(self) -> Set[str]: return set(self._fields)
[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)