import json from copy import deepcopy from pathlib import Path from PySide6.QtCore import QByteArray, QMimeData, Qt, Slot from PySide6.QtGui import QCloseEvent from PySide6.QtWidgets import ( QButtonGroup, QApplication, QFileDialog, QMainWindow, QMenu, QMessageBox, QTabWidget, ) from bedit.core.model import Component, Connection from bedit.core.application_log import get_logger from bedit.core.serializer import DocumentSerializer from bedit.core.simulation import Simulation from bedit.gui.controllers.document import DocumentController from bedit.gui.dialogs.component_options import ComponentOptionsDialog from bedit.gui.dialogs.graph_parameters import GraphParametersDialog from bedit.gui.dialogs.item_options import ItemOptionsDialog from bedit.gui.models.library_repository import LibraryRepository from bedit.gui.models.library_tree import ( COMPONENT_MIME_TYPE, COMPONENT_ROLE, COMPONENT_ID_ROLE, COMPONENT_INSTANCE_ROLE, ITEM_KIND_ROLE, DocumentTreeModel, LibraryTreeModel, ) from bedit.gui.graphics.workspace import SELECTION_MIME_TYPE from bedit.gui.dialogs.settings import SettingsDialog from bedit.gui.dialogs.port_options import PortOptionsDialog from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog from bedit.gui.dialogs.simulation_settings import SimulationSettingsDialog from bedit.gui.simulation_window import SimulationWindow from bedit.gui.preferences import application_settings from bedit.gui.simulation_reload import reload_simulation from bedit.gui.generated.ui_main_window import Ui_MainWindow from bedit.gui.application_log import ApplicationLogHandler class MainWindow(QMainWindow): """Application shell and owner of the single active document.""" def __init__(self) -> None: super().__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.application_logger = get_logger() self.log = get_logger("ui") self.log_handler = ApplicationLogHandler(self.ui.logOutput) self.application_logger.addHandler(self.log_handler) self.application_logger.setLevel("INFO") self.ui.workspaceVerticalSplitter.setSizes([580, 160]) self.log.info("BEdit started") self.settings = application_settings() self._applying_text_definition = False # Keep a Python-owned top-level window. Giving it MainWindow as its Qt # parent makes some Linux window managers inherit the BEdit window icon. self._simulation_window = SimulationWindow() self.libraries = LibraryRepository(self) self.simulation = Simulation( openmodelica_path=SettingsDialog.openmodelica_path(self.settings) ) self.document_controller = DocumentController(self, simulation=self.simulation) self.library_tree_model = LibraryTreeModel( self.libraries, self.document_controller, self, ) self.document_tree_model = DocumentTreeModel(self.document_controller, self) self._configure_models() QApplication.clipboard().dataChanged.connect(self._update_edit_actions) self._connect_actions() self._populate_view_menu() self._restore_window_geometry() self.ui.leftDockHost.setWindowFlags(Qt.WindowType.Widget) self.ui.leftDockHost.show() self.ui.panel_libraries.show() self.ui.panel_document.show() self.ui.leftDockHost.setTabPosition( Qt.DockWidgetArea.LeftDockWidgetArea, QTabWidget.TabPosition.North ) self.ui.leftDockHost.tabifyDockWidget(self.ui.panel_document, self.ui.panel_libraries) self.ui.panel_document.raise_() self.ui.workspaceSplitter.setSizes([280, 720]) self.reload_libraries() self._active_graph_changed() self._update_title() def _configure_models(self) -> None: self.ui.treeView.setModel(self.library_tree_model) self.ui.treeView.setHeaderHidden(True) self.ui.treeView.setDragEnabled(True) self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly) self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.ui.treeView.customContextMenuRequested.connect(self.show_external_library_context_menu) self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll) self.ui.documentTreeView.setModel(self.document_tree_model) self.ui.documentTreeView.setHeaderHidden(True) self.ui.documentTreeView.setDragEnabled(True) self.ui.documentTreeView.setDragDropMode(self.ui.documentTreeView.DragDropMode.DragOnly) self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu) self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component) self.ui.documentTreeView.clicked.connect( lambda _index: self._update_edit_actions() ) self.ui.treeView.clicked.connect(lambda _index: self._update_edit_actions()) self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll) self.ui.graphView.set_model(self.document_controller) self.ui.graphView.componentOptionsRequested.connect(self.show_component_options) self.ui.graphView.componentPortOptionsRequested.connect(self.show_component_port_options) self.ui.graphView.componentParameterOptionsRequested.connect( self.show_component_parameter_options ) self.ui.graphView.portOptionsRequested.connect(self.show_port_options) self.ui.graphView.connectionOptionsRequested.connect(self.show_connection_options) self.ui.graphView.selectionAvailabilityChanged.connect( lambda _available: self._update_edit_actions() ) self.ui.graphView.toolModeShortcutRequested.connect(self.set_graph_tool) self.mode_button_group = QButtonGroup(self) self.mode_button_group.setExclusive(True) self.mode_button_group.addButton(self.ui.pointerToolButton) self.mode_button_group.addButton(self.ui.connectToolButton) self.mode_button_group.addButton(self.ui.boxToolButton) self.mode_button_group.addButton(self.ui.lineToolButton) self.mode_button_group.addButton(self.ui.textToolButton) self.ui.navigateUpButton.clicked.connect(self.navigate_up) self.ui.navigateDownButton.clicked.connect(self.navigate_down) self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer")) self.ui.connectToolButton.clicked.connect(lambda: self.set_graph_tool("connect")) self.ui.boxToolButton.clicked.connect(lambda: self.set_graph_tool("box")) self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line")) self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text")) self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected) self.ui.textDefinitionEditor.definitionEdited.connect( self.apply_text_definition ) self.document_controller.activeGraphChanged.connect(self._active_graph_changed) self.document_controller.textDefinitionChanged.connect( self._text_definition_changed ) def _connect_actions(self) -> None: self.ui.actionNew.triggered.connect(self.new_document) self.ui.actionOpen.triggered.connect(self.open_document) self.ui.actionReloadLibraries.triggered.connect(self.reload_libraries) self.ui.actionReloadSimulation.triggered.connect(self.reload_simulation_code) self.ui.actionSave.triggered.connect(self.save_document) self.ui.actionSaveAs.triggered.connect(self.save_document_as) self.ui.actionClose.triggered.connect(self.close_document) self.ui.actionExit.triggered.connect(self.close) self.ui.actionSettings.triggered.connect(self.show_settings) self.ui.actionAbout.triggered.connect(self.show_about) self.ui.actionAboutQt.triggered.connect( lambda: QMessageBox.aboutQt(self, "About Qt Framework") ) self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo) self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo) self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected) self.ui.actionCopy.triggered.connect(self.copy_selection) self.ui.actionCut.triggered.connect(self.cut_selection) self.ui.actionPaste.triggered.connect(self.paste_selection) self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all) self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected) self.addAction(self.ui.actionRotateClockwise) self.ui.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in) self.ui.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out) self.ui.actionCenterView.triggered.connect(self.ui.graphView.center_workspace) self.ui.actionSimulationSettings.triggered.connect( self.show_simulation_settings ) self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters) self.ui.actionCompose.triggered.connect(self.compose_active_graph) self.ui.actionExportModel.triggered.connect(self.export_model) self.ui.actionSimulationWindow.triggered.connect(self.show_simulation_window) self.ui.actionRunSimulation.triggered.connect(self.run_simulation) self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled) self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled) self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title()) self.document_controller.filePathChanged.connect(lambda _path: self._update_title()) self.document_controller.documentOpenedChanged.connect(self._document_opened_changed) self.ui.actionUndo.setEnabled(False) self.ui.actionRedo.setEnabled(False) self._update_edit_actions() self._document_opened_changed(False) def _populate_view_menu(self) -> None: for panel in (self.ui.panel_document, self.ui.panel_libraries): self.ui.menuPanels.addAction(panel.toggleViewAction()) for toolbar in ( self.ui.fileToolbar, self.ui.editToolbar, self.ui.cameraToolbar, self.ui.simulationToolbar, ): self.ui.menuToolbars.addAction(toolbar.toggleViewAction()) def reload_libraries(self) -> None: self.libraries.load_paths(SettingsDialog.library_paths(self.settings)) self.ui.treeView.expandAll() self.log.info("Reloaded %d libraries", len(self.libraries.libraries)) if self.libraries.load_warnings: for warning in self.libraries.load_warnings: self.log.warning("Library load failed: %s", warning) QMessageBox.warning( self, "Some libraries could not be loaded", "\n".join(self.libraries.load_warnings), ) @Slot() def reload_simulation_code(self) -> None: try: replacement = reload_simulation(self.simulation) except Exception as error: self.log.exception("Could not reload simulation code") QMessageBox.critical(self, "Could not reload simulation code", str(error)) return self.simulation = replacement self.document_controller.simulation = replacement self.log.info("Reloaded simulation code") @Slot() def show_simulation_settings(self) -> None: component = self.document_controller.active_component if component is None or component.implementation_kind != "graph": return dialog = SimulationSettingsDialog(component.graph.simulation_settings, self) if dialog.exec() == dialog.DialogCode.Accepted: self.document_controller.edit_simulation_settings(dialog.settings) @Slot() def show_graph_parameters(self) -> None: component = self.document_controller.active_component if component is None or component.implementation_kind != "graph": return dialog = GraphParametersDialog(component, self) if dialog.exec() == dialog.DialogCode.Accepted: try: self.document_controller.edit_graph_parameter_values( component.id, dialog.parameter_values ) except ValueError as error: self.log.error("Could not change graph parameters: %s", error) QMessageBox.warning(self, "Cannot change graph parameters", str(error)) @Slot() def compose_active_graph(self) -> None: try: self.document_controller.compose_active_graph() except ValueError as error: self.log.error("Composition failed: %s", error) QMessageBox.warning(self, "Cannot compose", str(error)) @Slot() def export_model(self) -> None: try: model_name, source = self.document_controller.compose_active_graph_source() except ValueError as error: self.log.error("Model export composition failed: %s", error) QMessageBox.warning(self, "Cannot Export Model", str(error)) return file_name, _selected_filter = QFileDialog.getSaveFileName( self, "Export OpenModelica Model", f"{model_name}.mo", "Modelica Models (*.mo);;All Files (*)", ) if not file_name: return path = Path(file_name) if not path.suffix: path = path.with_suffix(".mo") temporary_path = path.with_suffix(path.suffix + ".tmp") try: temporary_path.write_text(source, encoding="utf-8") temporary_path.replace(path) except OSError as error: self.log.error("Could not export model %s: %s", path, error) QMessageBox.critical(self, "Cannot Export Model", str(error)) return self.log.info("Exported OpenModelica model to %s", path) @Slot() def show_simulation_window(self) -> None: self._simulation_window.show() self._simulation_window.raise_() self._simulation_window.activateWindow() @Slot() def run_simulation(self) -> None: window = self._simulation_window callbacks = window.begin_run() self.show_simulation_window() try: self.document_controller.run_simulation(*callbacks) window.prepare_run_model(self.simulation.model_name) except Exception as error: self.log.exception("Simulation run failed") window.report_start_error(error) def _restore_window_geometry(self) -> None: geometry = self.settings.value("window/geometry") if geometry is not None: self.restoreGeometry(geometry) def _update_title(self) -> None: if self.document_controller.document is None: self.setWindowTitle("BEdit") return name = ( self.document_controller.file_path.name if self.document_controller.file_path else "Untitled" ) modified = "*" if not self.document_controller.undo_stack.isClean() else "" self.setWindowTitle(f"{modified}{name} — BEdit") def _active_graph_changed(self) -> None: component = self.document_controller.active_component if component is None: self.ui.graphBreadcrumbLabel.setText("No component selected") self.ui.navigateUpButton.setEnabled(False) self.ui.workspaceModeLabel.setText("") self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage) self._set_graph_controls_visible(False) self.ui.actionSimulationSettings.setEnabled(False) self.ui.actionGraphParameters.setEnabled(False) self.ui.actionCompose.setEnabled(False) self.ui.actionExportModel.setEnabled(False) self.ui.actionRunSimulation.setEnabled(False) self._update_edit_actions() return self.ui.graphBreadcrumbLabel.setText(" › ".join(self.document_controller.breadcrumb())) self.ui.navigateUpButton.setEnabled( self.document_controller.document.find_parent(component.id) is not None ) is_graph = component.implementation_kind == "graph" self.ui.actionSimulationSettings.setEnabled(is_graph) self.ui.actionGraphParameters.setEnabled(is_graph) self.ui.actionCompose.setEnabled(is_graph) self.ui.actionExportModel.setEnabled(is_graph) self.ui.actionRunSimulation.setEnabled(is_graph) self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceStack.setCurrentWidget( self.ui.graphPage if is_graph else self.ui.textPage ) self._set_graph_controls_visible(is_graph) if is_graph: self.set_graph_tool("pointer") else: self._load_text_definition() self._update_edit_actions() def _update_edit_actions(self) -> None: component = self.document_controller.active_component is_graph = component is not None and component.implementation_kind == "graph" has_selection = bool( self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems() ) document_component_selected = bool( self.ui.documentTreeView.currentIndex().data(COMPONENT_ID_ROLE) ) library_component_selected = isinstance( self.ui.treeView.currentIndex().data(COMPONENT_ROLE), dict ) document_has_focus = self._view_has_focus(self.ui.documentTreeView) library_has_focus = self._view_has_focus(self.ui.treeView) self.ui.actionCopy.setEnabled( (is_graph and has_selection) or (document_has_focus and document_component_selected) or (library_has_focus and library_component_selected) ) self.ui.actionCut.setEnabled( (is_graph and has_selection) or (document_has_focus and document_component_selected) ) self.ui.actionDelete.setEnabled(is_graph and has_selection) self.ui.actionRotateClockwise.setEnabled( is_graph and self.ui.graphView.has_selected_components() ) self.ui.rotateToolButton.setEnabled( is_graph and self.ui.graphView.has_selected_components() ) self.ui.actionSelectAll.setEnabled(is_graph) can_paste_component = QApplication.clipboard().mimeData().hasFormat( COMPONENT_MIME_TYPE ) or QApplication.clipboard().mimeData().hasFormat(SELECTION_MIME_TYPE) self.ui.actionPaste.setEnabled( (is_graph and not document_has_focus and not library_has_focus) or ( document_has_focus and self.document_controller.document is not None and can_paste_component ) ) self.ui.navigateDownButton.setEnabled( is_graph and self.ui.graphView.has_single_selected_component() ) def copy_selection(self) -> bool: if self._view_has_focus(self.ui.documentTreeView): return self._copy_tree_component(self.ui.documentTreeView) if self._view_has_focus(self.ui.treeView): return self._copy_tree_component(self.ui.treeView) return self.ui.graphView.copy_selection() def cut_selection(self) -> None: if self._view_has_focus(self.ui.documentTreeView): index = self.ui.documentTreeView.currentIndex() component_id = index.data(COMPONENT_ID_ROLE) if component_id and self._copy_tree_component(self.ui.documentTreeView): self.document_controller.delete_component(component_id) return if self._view_has_focus(self.ui.treeView): self._copy_tree_component(self.ui.treeView) return self.ui.graphView.cut_selection() def paste_selection(self) -> None: if self._view_has_focus(self.ui.documentTreeView): self._paste_into_document_tree() return self.ui.graphView.paste_selection() def _copy_tree_component(self, tree) -> bool: component = tree.currentIndex().data(COMPONENT_ROLE) if not isinstance(component, dict): return False mime_data = QMimeData() mime_data.setData( COMPONENT_MIME_TYPE, QByteArray(json.dumps(component).encode("utf-8")), ) QApplication.clipboard().setMimeData(mime_data) return True def _paste_into_document_tree(self) -> None: index = self.ui.documentTreeView.currentIndex() kind = index.data(ITEM_KIND_ROLE) owner_id = None if kind == "current-component": component_id = index.data(COMPONENT_ID_ROLE) component = ( self.document_controller.document.find_component(component_id) if self.document_controller.document else None ) if component is None or component.implementation_kind != "graph": QMessageBox.warning( self, "Cannot Paste", "Select a graph component or the document root." ) return owner_id = component.id elif kind != "current-document": QMessageBox.warning( self, "Cannot Paste", "Select a graph component or the document root." ) return try: components, connections = self._clipboard_components() self.document_controller.paste_components_to( owner_id, components, connections ) except ValueError as error: QMessageBox.warning(self, "Cannot Paste", str(error)) @staticmethod def _clipboard_components() -> tuple[list[Component], list]: mime_data = QApplication.clipboard().mimeData() try: if mime_data.hasFormat(SELECTION_MIME_TYPE): payload = json.loads( bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8") ) return ( [Component.from_dict(item) for item in payload.get("components", [])], [Connection.from_dict(item) for item in payload.get("connections", [])], ) if mime_data.hasFormat(COMPONENT_MIME_TYPE): payload = json.loads( bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8") ) return [Component.from_dict(payload)], [] except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error: raise ValueError("The clipboard does not contain a valid component") from error raise ValueError("The clipboard does not contain a BEdit component") @staticmethod def _view_has_focus(view) -> bool: focus = QApplication.focusWidget() return focus is view or (focus is not None and view.isAncestorOf(focus)) @Slot() def navigate_up(self) -> None: if self._resolve_source_edits(): self.document_controller.navigate_up() @Slot() def navigate_down(self) -> None: if self._resolve_source_edits(): self.ui.graphView.open_selected_component() def _set_graph_controls_visible(self, visible: bool) -> None: for widget in ( self.ui.pointerToolButton, self.ui.connectToolButton, self.ui.boxToolButton, self.ui.lineToolButton, self.ui.textToolButton, self.ui.rotateToolButton, ): widget.setVisible(visible) def set_graph_tool(self, mode: str) -> None: self.ui.graphView.set_tool_mode(mode) { "pointer": self.ui.pointerToolButton, "connect": self.ui.connectToolButton, "box": self.ui.boxToolButton, "line": self.ui.lineToolButton, "text": self.ui.textToolButton, }[mode].setChecked(True) def _load_text_definition(self) -> None: component = self.document_controller.active_component if component is None: return self.ui.textDefinitionEditor.set_definition( component.source.get("declarations", ""), component.source.get("initialEquations", ""), component.source.get("equations", ""), component.inputs, component.outputs, component.parameters, ) def _resolve_source_edits(self) -> bool: component = self.document_controller.active_component if ( component is None or component.implementation_kind != "text" or not self.ui.textDefinitionEditor.is_modified ): return True answer = QMessageBox.question( self, "Apply text component changes?", "The text component has unapplied declaration, initial-equation, " "equation, port, or parameter changes.", QMessageBox.StandardButton.Apply | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, ) if answer == QMessageBox.StandardButton.Apply: return self.apply_text_definition() return answer == QMessageBox.StandardButton.Discard @Slot() def apply_text_definition(self) -> bool: try: inputs, outputs = self.ui.textDefinitionEditor.ports self._applying_text_definition = True try: self.document_controller.replace_active_text_definition( inputs, outputs, self.ui.textDefinitionEditor.declarations, self.ui.textDefinitionEditor.initial_equations, self.ui.textDefinitionEditor.equations, self.ui.textDefinitionEditor.parameters, ) finally: self._applying_text_definition = False except (TypeError, ValueError) as error: self.log.error("Text component update failed: %s", error) QMessageBox.critical(self, "Invalid text component", str(error)) self._load_text_definition() return False self.ui.textDefinitionEditor.set_modified(False) return True @Slot(str) def _text_definition_changed(self, component_id: str) -> None: component = self.document_controller.active_component if ( component is not None and component.id == component_id and not self._applying_text_definition ): self._load_text_definition() def _maybe_save(self) -> bool: if self.document_controller.document is None: return True if self.document_controller.undo_stack.isClean(): return True answer = QMessageBox.warning( self, "Unsaved changes", "The current graph contains unsaved changes.", QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, ) if answer == QMessageBox.StandardButton.Save: return self.save_document() return answer == QMessageBox.StandardButton.Discard @Slot() def new_document(self) -> None: if self._resolve_source_edits() and self._maybe_save(): self.document_controller.new_document() self.log.info("Created a new document") @Slot() def close_document(self) -> None: if self._resolve_source_edits() and self._maybe_save(): path = self.document_controller.file_path self.document_controller.close_document() self.log.info("Closed document%s", f" {path}" if path else "") @Slot() def open_document(self) -> None: if not self._resolve_source_edits() or not self._maybe_save(): return filename, _ = QFileDialog.getOpenFileName( self, "Open graph", "", "BEdit documents (*.bedit.json *.json *.beb);;" "BEdit JSON (*.bedit.json *.json);;BEdit Binary (*.beb);;All files (*)", ) if not filename: return try: self.document_controller.load(Path(filename)) self.log.info("Opened document %s", filename) except (OSError, ValueError) as error: self.log.error("Could not open document %s: %s", filename, error) QMessageBox.critical(self, "Could not open graph", str(error)) @Slot() def save_document(self) -> bool: if self.document_controller.document is None: return False if self.document_controller.file_path is None: return self.save_document_as() try: path = self.document_controller.save() self.log.info("Saved document %s", path) except OSError as error: self.log.error("Could not save document: %s", error) QMessageBox.critical(self, "Could not save graph", str(error)) return False return True @Slot() def save_document_as(self) -> bool: if self.document_controller.document is None: return False filename, selected_filter = QFileDialog.getSaveFileName( self, "Save graph", "untitled.beb", "BEdit Binary (*.beb);;BEdit JSON (*.bedit.json *.json);;All files (*)", ) if not filename: return False path = Path(filename) if path.suffix.lower() not in {".json", ".beb"}: path = path.with_suffix( ".beb" if "Binary" in selected_filter else ".bedit.json" ) try: path = self.document_controller.save(path) self.log.info("Saved document %s", path) except OSError as error: self.log.error("Could not save document %s: %s", filename, error) QMessageBox.critical(self, "Could not save graph", str(error)) return False return True @Slot() def show_settings(self) -> None: dialog = SettingsDialog(self) dialog.settingsChanged.connect(self.reload_libraries) dialog.settingsChanged.connect(self.refresh_editor_settings) dialog.settingsChanged.connect(self.refresh_simulation_preferences) dialog.exec() def refresh_simulation_preferences(self) -> None: self.simulation.openmodelica_path = SettingsDialog.openmodelica_path( self.settings ) def refresh_editor_settings(self) -> None: scene = self.ui.graphView.scene() if scene is not None: scene.update() self.ui.graphView.viewport().update() self.ui.textDefinitionEditor.ui.declarationsEdit.reload_highlighting() self.ui.textDefinitionEditor.ui.initialEquationsEdit.reload_highlighting() self.ui.textDefinitionEditor.ui.equationsEdit.reload_highlighting() def _document_opened_changed(self, opened: bool) -> None: for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs): action.setEnabled(opened) self._active_graph_changed() @Slot(object) def activate_tree_component(self, index) -> None: if index.data(ITEM_KIND_ROLE) != "current-component": return component_id = index.data(COMPONENT_ID_ROLE) if component_id: self.document_controller.activate_component(component_id) @Slot(object) def show_library_context_menu(self, position) -> None: tree_view = self.ui.documentTreeView index = tree_view.indexAt(position) if index.isValid(): tree_view.setCurrentIndex(index) kind = index.data(ITEM_KIND_ROLE) if kind == "current-component": component_id = index.data(COMPONENT_ID_ROLE) document = self.document_controller.document component = document.find_component(component_id) if document else None if component is None: return menu = QMenu(self) graph_action = None text_action = None if component.implementation_kind == "graph": graph_action = menu.addAction("New Graph Block") text_action = menu.addAction("New Text Block") menu.addSeparator() options_action = menu.addAction("Component Options…") ports_action = menu.addAction("Port Options…") parameters_action = menu.addAction("Parameter Options…") menu.addSeparator() copy_action = menu.addAction("Copy") cut_action = menu.addAction("Cut") paste_action = menu.addAction("Paste") paste_action.setEnabled(component.implementation_kind == "graph") delete_action = menu.addAction("Delete") selected = menu.exec(tree_view.viewport().mapToGlobal(position)) if graph_action is not None and selected is graph_action: self.document_controller.add_child(component_id, "graph") elif text_action is not None and selected is text_action: self.document_controller.add_child(component_id, "text") elif selected is options_action: self.show_component_options(component_id) elif selected is ports_action: self.show_component_port_options(component_id) elif selected is parameters_action: self.show_component_parameter_options(component_id) elif selected is copy_action: self._copy_tree_component(tree_view) elif selected is cut_action: if self._copy_tree_component(tree_view): self.document_controller.delete_component(component_id) elif selected is paste_action: self._paste_into_document_tree() elif selected is delete_action: answer = QMessageBox.question( self, "Delete component?", f"Delete {component.name!r} and all of its contents?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if answer == QMessageBox.StandardButton.Yes: self.document_controller.delete_component(component_id) return if kind != "current-document": return menu = QMenu(self) graph_action = menu.addAction("New Graph Block") text_action = menu.addAction("New Text Block") menu.addSeparator() paste_action = menu.addAction("Paste") selected = menu.exec(tree_view.viewport().mapToGlobal(position)) if selected is graph_action: self.document_controller.add_root("graph") elif selected is text_action: self.document_controller.add_root("text") elif selected is paste_action: self._paste_into_document_tree() @Slot(object) def show_external_library_context_menu(self, position) -> None: tree = self.ui.treeView index = tree.indexAt(position) if index.isValid(): tree.setCurrentIndex(index) component = index.data(COMPONENT_INSTANCE_ROLE) if not isinstance(component, Component): return menu = QMenu(self) copy_action = menu.addAction("Copy") menu.addSeparator() ports_action = menu.addAction("Port Options…") parameters_action = menu.addAction("Parameter Options…") selected = menu.exec(tree.viewport().mapToGlobal(position)) if selected is copy_action: self._copy_tree_component(tree) elif selected is ports_action: dialog = PortOptionsDialog(component, self) if dialog.exec() == dialog.DialogCode.Accepted: old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs) component.inputs = dialog.inputs component.outputs = dialog.outputs library = next( ( library for library in self.libraries.libraries if any(item is component for item in library.document.all_components()) ), None, ) try: if library is not None: library.document.validate() DocumentSerializer.save(library.document, Path(library.source_path)) except (OSError, ValueError) as error: component.inputs, component.outputs = old_inputs, old_outputs self.log.error("Could not change library ports: %s", error) QMessageBox.warning(self, "Cannot change library ports", str(error)) self.library_tree_model.rebuild() elif selected is parameters_action: dialog = ParameterOptionsDialog(component, self) if dialog.exec() == dialog.DialogCode.Accepted: old_parameters = deepcopy(component.parameters) component.parameters = dialog.parameters library = next( ( library for library in self.libraries.libraries if any(item is component for item in library.document.all_components()) ), None, ) try: if library is not None: library.document.validate() DocumentSerializer.save( library.document, Path(library.source_path) ) except (OSError, ValueError) as error: component.parameters = old_parameters self.log.error("Could not change library parameters: %s", error) QMessageBox.warning( self, "Cannot change library parameters", str(error) ) self.library_tree_model.rebuild() @Slot(str) def show_component_port_options(self, component_id: str) -> None: document = self.document_controller.document component = document.find_component(component_id) if document else None if component is None: return dialog = PortOptionsDialog(component, self) if dialog.exec() != dialog.DialogCode.Accepted: return try: self.document_controller.edit_component_ports( component_id, dialog.inputs, dialog.outputs ) except ValueError as error: self.log.error("Could not change component ports: %s", error) QMessageBox.warning(self, "Cannot change ports", str(error)) @Slot(str) def show_component_parameter_options(self, component_id: str) -> None: document = self.document_controller.document component = document.find_component(component_id) if document else None if component is None: return dialog = ParameterOptionsDialog(component, self) if dialog.exec() != dialog.DialogCode.Accepted: return try: self.document_controller.edit_component_parameters( component_id, dialog.parameters ) except ValueError as error: self.log.error("Could not change component parameters: %s", error) QMessageBox.warning(self, "Cannot change parameters", str(error)) @Slot(str) def show_component_options(self, component_id: str) -> None: document = self.document_controller.document component = document.find_component(component_id) if document else None if component is None: return dialog = ComponentOptionsDialog(component, self) if dialog.exec() == dialog.DialogCode.Accepted: try: self.document_controller.edit_component_appearance( component_id, dialog.ui.nameEdit.text().strip(), dialog.edited_icon, dialog.edited_inputs, dialog.edited_outputs, dialog.ui.showSubtreeCheckBox.isChecked(), dialog.ui.showNameCheckBox.isChecked(), ) except ValueError as error: self.log.error("Could not rename component: %s", error) QMessageBox.warning(self, "Cannot rename component", str(error)) @Slot(str, str) def show_port_options(self, port_id: str, direction: str) -> None: owner = self.document_controller.active_component if owner is None: return ports = owner.inputs if direction == "input" else owner.outputs port = next((item for item in ports if item.id == port_id), None) if port is None: return dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self) if dialog.exec() == dialog.DialogCode.Accepted: self.document_controller.rename_interface_port(port_id, dialog.name) @Slot(str) def show_connection_options(self, connection_id: str) -> None: owner = self.document_controller.active_component if owner is None or owner.implementation_kind != "graph": return connection = owner.graph.connections.get(connection_id) if connection is None: return dialog = ItemOptionsDialog( "Connection Options", connection.name, self, name_required=False, show_name=bool(connection.properties.get("showName", False)), ) if dialog.exec() == dialog.DialogCode.Accepted: self.document_controller.edit_connection_options( connection_id, dialog.name, dialog.show_name ) @Slot() def show_about(self) -> None: QMessageBox.about( self, "About BEdit", "
A graphical editor built with Python and Qt.
", ) def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt API name) if not self._resolve_source_edits() or not self._maybe_save(): event.ignore() return self.settings.setValue("window/geometry", self.saveGeometry()) self._simulation_window.close() self.simulation.shutdown() self.log.info("BEdit closed") self.application_logger.removeHandler(self.log_handler) event.accept()