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)}')