Source code for qudi.tools.config_editor.module_widgets
# -*- coding: utf-8 -*-
"""
QWidgets for configuring the individual module config sections
Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
distribution and on <https://github.com/Ulm-IQO/qudi-core/>
This file is part of qudi.
Qudi is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with qudi.
If not, see <https://www.gnu.org/licenses/>.
"""
__all__ = ['LocalModuleConfigWidget', 'RemoteModuleConfigWidget', 'ModuleConnectorsWidget',
'ModuleOptionsWidget']
import copy
from PySide2 import QtCore, QtWidgets
from typing import Optional, Iterable, Mapping, Dict, Sequence, Union, Any, Tuple, List
from qudi.core import Connector, ConfigOption
from qudi.core.config.validator import validate_remote_module_config, validate_local_module_config
from qudi.core.config.validator import ValidationError
from qudi.util.widgets.separator_lines import HorizontalLine
from qudi.util.widgets.path_line_edit import PathLineEdit
from qudi.tools.config_editor.custom_widgets import CustomOptionsWidget, CustomConnectorsWidget
[docs]
class ModuleConnectorsWidget(QtWidgets.QWidget):
"""
"""
[docs]
def __init__(self,
mandatory_targets: Optional[Mapping[str, Sequence[str]]] = None,
optional_targets: Optional[Mapping[str, Sequence[str]]] = None,
module_names: Optional[Iterable[str]] = None,
config: Optional[Mapping[str, str]] = None,
parent: Optional[QtWidgets.QWidget] = None
) -> None:
super().__init__(parent=parent)
if mandatory_targets is None:
self._mandatory_targets = dict()
else:
self._mandatory_targets = copy.deepcopy(mandatory_targets)
if optional_targets is None:
self._optional_targets = dict()
else:
self._optional_targets = copy.deepcopy(optional_targets)
if set(self._mandatory_targets).intersection(self._optional_targets):
raise ValueError('Connector names can not be both mandatory AND optional')
layout = QtWidgets.QGridLayout()
layout.setColumnStretch(1, 1)
self.setLayout(layout)
# Create Caption
label = QtWidgets.QLabel('Connectors')
label.setAlignment(QtCore.Qt.AlignCenter)
font = label.font()
font.setBold(True)
label.setFont(font)
layout.addWidget(label, 0, 0, 1, 2)
# Keep track of connector editor widgets
self._connector_editors = dict()
# Create mandatory connectors
for row, (name, targets) in enumerate(self._mandatory_targets.items(), 1):
label, editor = self._make_conn_widgets(name, targets, False)
layout.addWidget(label, row, 0)
layout.addWidget(editor, row, 1)
self._connector_editors[name] = editor
# Create optional connectors
offset = len(self._connector_editors) + 1
for row, (name, targets) in enumerate(self._optional_targets.items(), offset):
label, editor = self._make_conn_widgets(name, targets, True)
layout.addWidget(label, row, 0)
layout.addWidget(editor, row, 1)
self._connector_editors[name] = editor
# Add separator
layout.addWidget(HorizontalLine(), len(self._connector_editors) + 1, 0, 1, 2)
# Create custom connector editor
self.custom_connectors_widget = CustomConnectorsWidget(
forbidden_names=list(self._connector_editors),
module_names=module_names
)
layout.addWidget(self.custom_connectors_widget, len(self._connector_editors) + 2, 0, 1, 2)
layout.setRowStretch(len(self._connector_editors) + 3, 1)
self.set_config(config)
@property
def config(self) -> Dict[str, Union[None, str]]:
conn = {name: editor.currentText() for name, editor in self._connector_editors.items()}
conn.update(self.custom_connectors_widget.config)
return {name: target if target else None for name, target in conn.items()}
def set_config(self, config: Union[None, Mapping[str, Union[None, str]]]) -> None:
if config is None:
for editor in self._connector_editors.values():
editor.setCurrentIndex(0)
self.custom_connectors_widget.set_config(None)
else:
cfg = config.copy()
for name, editor in self._connector_editors.items():
target = cfg.pop(name, editor.currentText())
index = max(0, editor.findText(target)) if target else 0
editor.setCurrentIndex(index)
# Remaining connectors are custom
self.custom_connectors_widget.set_config(cfg)
@staticmethod
def _make_conn_widgets(name: str,
targets: Sequence[str],
optional: bool
) -> Tuple[QtWidgets.QLabel, QtWidgets.QComboBox]:
label = QtWidgets.QLabel(f'{name}:' if optional else f'* {name}:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
label.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
editor = QtWidgets.QComboBox()
editor.addItem('')
editor.addItems(targets)
editor.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
return label, editor
[docs]
class ModuleOptionsWidget(QtWidgets.QWidget):
"""
"""
[docs]
def __init__(self,
mandatory_names: Optional[Iterable[str]] = None,
optional_names: Optional[Iterable[str]] = None,
config: Optional[Mapping[str, Any]] = None,
parent: Optional[QtWidgets.QWidget] = None
) -> None:
super().__init__(parent=parent)
self._mandatory_names = list() if mandatory_names is None else list(mandatory_names)
self._optional_names = list() if optional_names is None else list(optional_names)
if set(self._mandatory_names).intersection(self._optional_names):
raise ValueError('ConfigOption names can not be both mandatory AND optional')
layout = QtWidgets.QGridLayout()
layout.setColumnStretch(1, 1)
self.setLayout(layout)
# Create Caption
label = QtWidgets.QLabel('ConfigOptions')
label.setAlignment(QtCore.Qt.AlignCenter)
font = label.font()
font.setBold(True)
label.setFont(font)
layout.addWidget(label, 0, 0, 1, 2)
# Keep track of option editor widgets
self._option_editors = dict()
# Create mandatory options
for row, name in enumerate(self._mandatory_names, 1):
label, editor = self._make_option_widgets(name, False)
layout.addWidget(label, row, 0)
layout.addWidget(editor, row, 1)
self._option_editors[name] = editor
# Create optional options
offset = len(self._option_editors) + 1
for row, name in enumerate(self._optional_names, offset):
label, editor = self._make_option_widgets(name, True)
layout.addWidget(label, row, 0)
layout.addWidget(editor, row, 1)
self._option_editors[name] = editor
# Add separator
layout.addWidget(HorizontalLine(), len(self._option_editors) + 1, 0, 1, 2)
# Create custom options editor
self.custom_options_widget = CustomOptionsWidget(forbidden_names=list(self._option_editors))
layout.addWidget(self.custom_options_widget, len(self._option_editors) + 2, 0, 1, 2)
layout.setRowStretch(len(self._option_editors) + 3, 1)
self.set_config(config)
@property
def config(self) -> Dict[str, Any]:
cfg = dict()
for name, editor in self._option_editors.items():
text = editor.text().strip()
if text == '':
# Interpret empty text as None for mandatory options. Skip missing optional options.
if name in self._optional_names:
continue
else:
cfg[name] = None
else:
# Try to parse text with eval(). If that fails, interpret text as plain string.
try:
cfg[name] = eval(text)
except (NameError, SyntaxError, ValueError):
cfg[name] = text
return cfg
def set_config(self, config: Union[None, Mapping[str, Any]]) -> None:
if config is None:
for editor in self._option_editors.values():
editor.setText('')
self.custom_options_widget.set_config(None)
else:
cfg = config.copy()
for name, editor in self._option_editors.items():
try:
editor.setText(repr(cfg.pop(name)))
except:
editor.setText('')
# Remaining options are custom
self.custom_options_widget.set_config(cfg)
@staticmethod
def _make_option_widgets(name: str,
optional: bool
) -> Tuple[QtWidgets.QLabel, QtWidgets.QLineEdit]:
label = QtWidgets.QLabel(f'{name}:' if optional else f'* {name}:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
label.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
editor = QtWidgets.QLineEdit()
editor.setPlaceholderText('text parsed by eval()')
return label, editor
[docs]
class LocalModuleConfigWidget(QtWidgets.QWidget):
"""
"""
[docs]
def __init__(self,
module_class: str,
config_options: Sequence[ConfigOption],
connectors: Sequence[Connector],
valid_connector_targets: Mapping[str, Sequence[str]],
named_modules: Mapping[str, str],
config: Optional[Dict[str, Union[str, bool, Dict[str, str], Dict[str, Any]]]] = None,
parent: Optional[QtWidgets.QWidget] = None
) -> None:
super().__init__(parent=parent)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setStretch(2, 1)
self.setLayout(layout)
# Module label
sub_layout = QtWidgets.QGridLayout()
sub_layout.setColumnStretch(1, 1)
layout.addLayout(sub_layout)
label = QtWidgets.QLabel('module.Class:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self._module_label = QtWidgets.QLabel(module_class)
font = self._module_label.font()
font.setBold(True)
self._module_label.setFont(font)
sub_layout.addWidget(label, 0, 0)
sub_layout.addWidget(self._module_label, 0, 1)
# allow_remote flag editor
label = QtWidgets.QLabel('Allow remote connection:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.allow_remote_checkbox = QtWidgets.QCheckBox()
self.allow_remote_checkbox.setToolTip(
'Allow other qudi instances to connect to this module via remote modules server.'
)
self.allow_remote_checkbox.toggled.connect(self._validate_and_mark_config)
sub_layout.addWidget(label, 1, 0)
sub_layout.addWidget(self.allow_remote_checkbox, 1, 1)
# Separator
layout.addWidget(HorizontalLine())
# Create splitter to spread options and connectors horizontally
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
self.splitter.setChildrenCollapsible(False)
self.splitter.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
layout.addWidget(self.splitter)
# Module Connectors editor
mandatory_targets, optional_targets = self._get_connector_targets(
connectors=connectors,
named_modules=named_modules,
valid_targets=valid_connector_targets
)
self.connectors_editor = ModuleConnectorsWidget(mandatory_targets=mandatory_targets,
optional_targets=optional_targets,
module_names=list(named_modules))
self.splitter.addWidget(self.connectors_editor)
# Module ConfigOptions editor
self.options_editor = ModuleOptionsWidget(
mandatory_names=[opt.name for opt in config_options if not opt.optional],
optional_names=[opt.name for opt in config_options if opt.optional]
)
self.splitter.addWidget(self.options_editor)
self.set_config(config)
self._validate_and_mark_config()
@property
def module_class(self) -> str:
return self._module_label.text()
@property
def config(self) -> Dict[str, Union[str, bool, Dict[str, str], Dict[str, Any]]]:
return {'module.Class': self.module_class,
'allow_remote': self.allow_remote_checkbox.isChecked(),
'options' : self.options_editor.config,
'connect' : self.connectors_editor.config}
def set_config(self,
config: Union[None, Dict[str, Union[str, bool, Dict[str, str], Dict[str, Any]]]]
) -> None:
if config:
self.allow_remote_checkbox.setChecked(config.get('allow_remote', False))
self.options_editor.set_config(config.get('options', dict()))
self.connectors_editor.set_config(config.get('connect', dict()))
else:
self.allow_remote_checkbox.setChecked(False)
self.options_editor.set_config(None)
self.connectors_editor.set_config(None)
@staticmethod
def _get_connector_targets(connectors: Sequence[Connector],
named_modules: Mapping[str, str],
valid_targets: Mapping[str, Sequence[str]]
) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]:
mandatory_targets = dict()
optional_targets = dict()
for conn in connectors:
targets = [
name for name, mod in named_modules.items() if mod in valid_targets[conn.name]
]
if conn.optional:
optional_targets[conn.name] = targets
else:
mandatory_targets[conn.name] = targets
return mandatory_targets, optional_targets
def validate_config(self) -> None:
validate_local_module_config(self.config)
@QtCore.Slot()
def _validate_and_mark_config(self) -> None:
try:
self.validate_config()
except ValidationError as err:
print(f'Invalid local module config. Problematic fields: {list(err.relative_path)}')
[docs]
class RemoteModuleConfigWidget(QtWidgets.QWidget):
"""
"""
[docs]
def __init__(self,
config: Optional[Mapping[str, Union[str, None]]] = None,
parent: Optional[QtWidgets.QWidget] = None
) -> None:
super().__init__(parent=parent)
layout = QtWidgets.QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setColumnStretch(1, 1)
layout.setRowStretch(5, 1)
self.setLayout(layout)
# remote name editor
label = QtWidgets.QLabel('* Native module name:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.native_name_lineedit = QtWidgets.QLineEdit()
self.native_name_lineedit.setToolTip('The native module name as configured on the remote '
'host qudi instance to connect to.')
self.native_name_lineedit.setPlaceholderText('Module name on remote host')
self.native_name_lineedit.textChanged.connect(self._validate_and_mark_config)
layout.addWidget(label, 0, 0)
layout.addWidget(self.native_name_lineedit, 0, 1)
# remote host editor
label = QtWidgets.QLabel('* Remote address:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.remote_host_lineedit = QtWidgets.QLineEdit('localhost')
self.remote_host_lineedit.setToolTip('The IP address of the remote host. Can also be '
'"localhost" for local qudi instances.')
self.remote_host_lineedit.setPlaceholderText('IP address or "localhost"')
self.remote_host_lineedit.textChanged.connect(self._validate_and_mark_config)
layout.addWidget(label, 1, 0)
layout.addWidget(self.remote_host_lineedit, 1, 1)
# remote port editor
label = QtWidgets.QLabel('* Remote port:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.remote_port_spinbox = QtWidgets.QSpinBox()
self.remote_port_spinbox.setRange(0, 65535)
self.remote_port_spinbox.setValue(12345)
self.remote_port_spinbox.setToolTip('Port to reach the remote host on.')
self.remote_port_spinbox.valueChanged.connect(self._validate_and_mark_config)
layout.addWidget(label, 2, 0)
layout.addWidget(self.remote_port_spinbox, 2, 1)
# certfile editor
label = QtWidgets.QLabel('Certificate file:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.certfile_lineedit = PathLineEdit(dialog_caption='Select SSL Certificate File',
follow_symlinks=True)
self.certfile_lineedit.setPlaceholderText('No certificate')
self.certfile_lineedit.setToolTip(
'SSL certificate file path for the remote module connection'
)
self.certfile_lineedit.textChanged.connect(self._validate_and_mark_config)
layout.addWidget(label, 3, 0)
layout.addWidget(self.certfile_lineedit, 3, 1)
# keyfile editor
label = QtWidgets.QLabel('Key file:')
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.keyfile_lineedit = PathLineEdit(dialog_caption='Select SSL Key File',
follow_symlinks=True)
self.keyfile_lineedit.setPlaceholderText('No key')
self.keyfile_lineedit.setToolTip('SSL key file path for the remote module server')
self.keyfile_lineedit.textChanged.connect(self._validate_and_mark_config)
layout.addWidget(label, 4, 0)
layout.addWidget(self.keyfile_lineedit, 4, 1)
self.set_config(config)
self._validate_and_mark_config()
@property
def config(self) -> Dict[str, Union[None, int, str]]:
native_module_name = self.native_name_lineedit.text()
host = self.remote_host_lineedit.text()
cfg = {'native_module_name': native_module_name if native_module_name else None,
'address' : host if host else None,
'port' : self.remote_port_spinbox.value()}
try:
cfg['certfile'] = self.certfile_lineedit.paths[0]
except IndexError:
pass
try:
cfg['keyfile'] = self.keyfile_lineedit.paths[0]
except IndexError:
pass
return cfg
def set_config(self, config: Union[None, Dict[str, Union[None, int, str]]]) -> None:
if config:
native_module_name = config.get('native_module_name', None)
host = config.get('address', None)
port = config.get('port', None)
try:
certfile = config['certfile']
keyfile = config['keyfile']
except KeyError:
certfile = keyfile = ''
if certfile is None or keyfile is None:
certfile = keyfile = ''
self.remote_host_lineedit.setText(host if host else '')
self.remote_port_spinbox.setValue(port if isinstance(port, int) else 12345)
self.native_name_lineedit.setText(native_module_name if native_module_name else '')
self.certfile_lineedit.setText(certfile)
self.certfile_lineedit.setText(keyfile)
else:
self.remote_host_lineedit.setText('')
self.remote_port_spinbox.setValue(12345)
self.native_name_lineedit.setText('')
self.certfile_lineedit.setText('')
self.certfile_lineedit.setText('')
def validate_config(self) -> None:
validate_remote_module_config(self.config)
@QtCore.Slot()
def _validate_and_mark_config(self) -> None:
try:
self.validate_config()
except ValidationError as err:
print(f'Invalid remote module config. Problematic fields: {list(err.relative_path)}')