Source code for qudi.tools.config_editor.config_editor

# -*- coding: utf-8 -*-

"""
Configuration editor App for creation and editing of qudi configuration files.

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__ = ('main', 'ConfigurationEditorMainWindow', 'ConfigurationEditor')

import os
import sys
from typing import Optional, Mapping, Dict, Any
from PySide2 import QtCore, QtGui, QtWidgets
from qudi.util.paths import get_main_dir, get_default_config_dir, get_artwork_dir
from qudi.core.config import Configuration

from qudi.tools.config_editor.module_selector import ModuleSelector
from qudi.tools.config_editor.module_editor import ModuleEditorWidget
from qudi.tools.config_editor.global_editor import GlobalEditorWidget
from qudi.tools.config_editor.tree_widgets import ConfigModulesTreeWidget
from qudi.tools.config_editor.module_finder import QudiModules

try:
    import matplotlib
    matplotlib.use('agg')
except ImportError:
    pass

# Enable the High DPI scaling support of Qt5
os.environ['QT_ENABLE_HIGHDPI_SCALING'] = '1'

if sys.platform == 'win32':
    # Set QT_LOGGING_RULES environment variable to suppress qt.svg related warnings that otherwise
    # spam the log due to some known Qt5 bugs, e.g. https://bugreports.qt.io/browse/QTBUG-52079
    os.environ['QT_LOGGING_RULES'] = 'qt.svg.warning=false'
else:
    # The following will prevent Qt to spam the logs on X11 systems with enough messages
    # to significantly slow the program down. Most of those warnings should have been
    # notice level or lower. This is a known problem since Qt does not fully comply to X11.
    os.environ['QT_LOGGING_RULES'] = '*.debug=false;*.info=false;*.notice=false;*.warning=false'


[docs] class ConfigurationEditor(QtWidgets.QMainWindow): """ """
[docs] def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent=parent) self.setWindowTitle('Qudi Config Editor') screen_size = QtWidgets.QApplication.instance().primaryScreen().availableSize() self.resize((screen_size.width() * 3) // 4, (screen_size.height() * 3) // 4) self.qudi_environment = QudiModules() self.module_tree_widget = ConfigModulesTreeWidget() self.module_tree_widget.itemChanged.connect(self._module_renamed_by_tree) self.module_tree_widget.itemSelectionChanged.connect(self._module_selection_changed) self.module_config_editor = ModuleEditorWidget(qudi_modules=self.qudi_environment) self.module_config_editor.sigModuleRenamed.connect(self._module_renamed_by_editor) self.global_config_editor = GlobalEditorWidget() label = QtWidgets.QLabel('Included Modules') label.setAlignment(QtCore.Qt.AlignCenter) font = label.font() font.setBold(True) font.setPointSize(font.pointSize() + 4) label.setFont(font) layout = QtWidgets.QVBoxLayout() layout.addWidget(label) layout.addWidget(self.module_tree_widget) layout.addWidget(self.global_config_editor) layout.setStretch(1, 1) left_widget = QtWidgets.QWidget() left_widget.setLayout(layout) label = QtWidgets.QLabel('Module Configuration') label.setAlignment(QtCore.Qt.AlignCenter) label.setFont(font) layout = QtWidgets.QVBoxLayout() layout.addWidget(label) layout.addWidget(self.module_config_editor) label = QtWidgets.QLabel('Mandatory fields/options/connectors are marked with *') font = label.font() font.setBold(True) label.setFont(font) label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(label) layout.setStretch(1, 1) right_widget = QtWidgets.QWidget() right_widget.setLayout(layout) splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) splitter.addWidget(left_widget) splitter.addWidget(right_widget) splitter.setChildrenCollapsible(False) splitter.setStretchFactor(0, 2) splitter.setStretchFactor(1, 3) self.setCentralWidget(splitter) # Main window actions icon_dir = os.path.join(get_main_dir(), 'artwork', 'icons') quit_icon = QtGui.QIcon(os.path.join(icon_dir, 'application-exit')) self.quit_action = QtWidgets.QAction(quit_icon, 'Quit') self.quit_action.setShortcut(QtGui.QKeySequence('Ctrl+Q')) load_icon = QtGui.QIcon(os.path.join(icon_dir, 'document-open')) self.load_action = QtWidgets.QAction(load_icon, 'Load') self.load_action.setShortcut(QtGui.QKeySequence('Ctrl+L')) self.load_action.setToolTip('Load a qudi configuration to edit from file.') save_icon = QtGui.QIcon(os.path.join(icon_dir, 'document-save')) self.save_action = QtWidgets.QAction(save_icon, 'Save') self.save_action.setShortcut(QtGui.QKeySequence('Ctrl+S')) self.save_action.setToolTip('Save the current qudi configuration to file.') self.save_as_action = QtWidgets.QAction('Save as ...') new_icon = QtGui.QIcon(os.path.join(icon_dir, 'document-new')) self.new_action = QtWidgets.QAction(new_icon, 'New') self.new_action.setShortcut(QtGui.QKeySequence('Ctrl+N')) self.new_action.setToolTip('Create a new qudi configuration from scratch.') select_icon = QtGui.QIcon(os.path.join(icon_dir, 'configure')) self.select_modules_action = QtWidgets.QAction(select_icon, 'Select Modules') self.select_modules_action.setShortcut(QtGui.QKeySequence('Ctrl+M')) self.select_modules_action.setToolTip( 'Open an editor to select the modules to include in config.' ) # Connect actions self.quit_action.triggered.connect(self.close) self.new_action.triggered.connect(self.new_config) self.load_action.triggered.connect(self.prompt_load_config) self.save_action.triggered.connect(self.save_config) self.save_as_action.triggered.connect(self.prompt_save_config) self.select_modules_action.triggered.connect(self.select_modules) # Create menu bar menu_bar = QtWidgets.QMenuBar() file_menu = QtWidgets.QMenu('File') menu_bar.addMenu(file_menu) file_menu.addAction(self.new_action) file_menu.addSeparator() file_menu.addAction(self.load_action) file_menu.addAction(self.save_action) file_menu.addAction(self.save_as_action) file_menu.addSeparator() file_menu.addAction(self.quit_action) file_menu = QtWidgets.QMenu('Edit') menu_bar.addMenu(file_menu) file_menu.addAction(self.select_modules_action) self.setMenuBar(menu_bar) # Create toolbar toolbar = QtWidgets.QToolBar() toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) toolbar.addAction(self.new_action) toolbar.addAction(self.load_action) toolbar.addAction(self.save_action) toolbar.addSeparator() toolbar.addAction(self.select_modules_action) toolbar.setFloatable(False) self.addToolBar(toolbar) # Process variables self._editor_item = None self._config_map = dict() self._current_file_path = None
@QtCore.Slot() def _module_selection_changed(self): selected_items = self.module_tree_widget.selectedItems() if not selected_items: self.module_config_editor.close_editor() self._editor_item = None return item = selected_items[0] if item is not self._editor_item: base = item.parent().text(0).lower() module = f'{base}.{item.text(2)}' name = item.text(1) # Get current module config dict config = self._config_map.get(base, dict()).get(name, None) # Sort out available connectors and targets as well as module config options if module == '<REMOTE MODULE>': self.module_config_editor.open_remote_module(name, config=config) else: self.module_config_editor.open_local_module( module_class=module, named_modules=self.module_tree_widget.modules[0], name=name, config=config ) self._editor_item = item @QtCore.Slot() def select_modules(self): self.module_config_editor.close_editor() available = self.qudi_environment.available_modules named_selected, unnamed_selected = self.module_tree_widget.modules selector_dialog = ModuleSelector(available_modules=available, named_modules=named_selected, unnamed_modules=unnamed_selected) if selector_dialog.exec_(): # Recycle old module names if identical modules are selected but not named new_named_selected, new_unnamed_selected = selector_dialog.selected_modules recycled_named_selected = { name: mod for name, mod in named_selected.items() if (name not in new_named_selected) and (mod in new_unnamed_selected) } new_named_selected.update(recycled_named_selected) for mod in recycled_named_selected.values(): new_unnamed_selected.remove(mod) # Set modules in main window self.module_tree_widget.set_modules(named_modules=new_named_selected, unnamed_modules=new_unnamed_selected) def new_config(self): self._current_file_path = None self._config_map = dict() self.module_config_editor.close_editor() self.module_tree_widget.set_modules(dict()) self.global_config_editor.open_editor(None) self.select_modules() def prompt_load_config(self): file_path = QtWidgets.QFileDialog.getOpenFileName( self, 'Qudi Config Editor: Load Configuration...', get_default_config_dir(), 'Config files (*.cfg)')[0] if file_path: self.module_config_editor.close_editor() config = Configuration() config.load(file_path, set_default=False) self._config_map = config.config_map self._current_file_path = file_path modules = self._get_modules_from_config(self._config_map) global_cfg = self._config_map.get('global', dict()) self.module_tree_widget.set_modules(named_modules=modules) self.global_config_editor.open_editor(global_cfg) def prompt_save_config(self): file_path = QtWidgets.QFileDialog.getSaveFileName( self, 'Qudi Config Editor: Save Configuration...', get_default_config_dir() if self._current_file_path is None else os.path.dirname( self._current_file_path), 'Config files (*.cfg)')[0] if file_path: config = Configuration(config=self._config_map) config.dump(file_path) self._current_file_path = file_path def prompt_overwrite(self, file_path): answer = QtWidgets.QMessageBox.question( self, 'Qudi Config Editor: Overwrite?', f'Do you really want to overwrite existing Qudi configuration at\n"{file_path}"?', QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) return answer == QtWidgets.QMessageBox.Yes def save_config(self): if self._current_file_path is None: self.prompt_save_config() elif os.path.exists(self._current_file_path): if self.prompt_overwrite(self._current_file_path): config = Configuration(config=self._config_map) config.dump(self._current_file_path) else: self.prompt_save_config() else: config = Configuration(config=self._config_map) config.dump(self._current_file_path) def prompt_close(self): answer = QtWidgets.QMessageBox.question( self, 'Qudi Config Editor: Quit?', 'Do you really want to quit the Qudi configuration editor?\nAll unsaved work will be ' 'lost.', QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No ) return answer == QtWidgets.QMessageBox.Yes def closeEvent(self, event): if self.prompt_close(): event.accept() else: event.ignore() @staticmethod def _get_modules_from_config(config: Mapping[str, Any]) -> Dict[str, str]: modules = {name: 'gui.' + cfg.get('module.Class', '<REMOTE MODULE>') for name, cfg in config.get('gui', dict()).items()} modules.update({name: 'logic.' + cfg.get('module.Class', '<REMOTE MODULE>') for name, cfg in config.get('logic', dict()).items()}) modules.update( {name: 'hardware.' + cfg.get('module.Class', '<REMOTE MODULE>') for name, cfg in config.get('hardware', dict()).items()} ) return modules @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) def _module_renamed_by_tree(self, item, column): if column != 1 or item is None or item.parent() is None: return name = item.text(1) if item is self._editor_item: self.module_config_editor.set_module_name(name) @QtCore.Slot(str) def _module_renamed_by_editor(self, name: str) -> None: self.module_tree_widget.blockSignals(True) try: self._editor_item.setText(1, name) except AttributeError: pass finally: self.module_tree_widget.blockSignals(False) def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: if event.key() == QtCore.Qt.Key_Escape: self.module_tree_widget.clearSelection() event.accept() else: super().keyPressEvent(event)
[docs] class ConfigurationEditorApp(QtWidgets.QApplication): """ """
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) app_icon = QtGui.QIcon(os.path.join(get_artwork_dir(), 'logo', 'logo-qudi.svg')) self.setWindowIcon(app_icon)
[docs] def main(): app = ConfigurationEditorApp(sys.argv) # init editor QMainWindow and show editor = ConfigurationEditor() editor.show() # Start event loop sys.exit(app.exec_())
if __name__ == '__main__': main()