""" B-ASIC Signal Flow Graph Editor Module. This file opens the main SFG editor window of the GUI for B-ASIC when run. """ import importlib.util import logging import os import sys import webbrowser from collections import deque from types import ModuleType from typing import TYPE_CHECKING, Deque, Dict, List, Optional, Sequence, Tuple, cast from qtpy.QtCore import QCoreApplication, QFileInfo, QSettings, QSize, Qt, QThread, Slot from qtpy.QtGui import QCursor, QIcon, QKeySequence, QPainter from qtpy.QtWidgets import ( QAction, QApplication, QFileDialog, QGraphicsItem, QGraphicsScene, QGraphicsTextItem, QGraphicsView, QInputDialog, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QShortcut, QStatusBar, ) import b_asic.core_operations import b_asic.special_operations from b_asic._version import __version__ from b_asic.GUI._preferences import GAP, GRID, MINBUTTONSIZE, PORTHEIGHT from b_asic.GUI.arrow import Arrow from b_asic.GUI.drag_button import DragButton from b_asic.GUI.gui_interface import Ui_main_window from b_asic.GUI.port_button import PortButton from b_asic.GUI.precedence_graph_window import PrecedenceGraphWindow from b_asic.GUI.select_sfg_window import SelectSFGWindow from b_asic.GUI.simulate_sfg_window import SimulateSFGWindow from b_asic.GUI.simulation_worker import SimulationWorker from b_asic.GUI.util_dialogs import FaqWindow, KeybindingsWindow from b_asic.gui_utils.about_window import AboutWindow from b_asic.gui_utils.decorators import decorate_class, handle_error from b_asic.gui_utils.icons import get_icon from b_asic.gui_utils.plot_window import PlotWindow from b_asic.operation import Operation from b_asic.port import InputPort, OutputPort from b_asic.save_load_structure import python_to_sfg, sfg_to_python from b_asic.signal import Signal from b_asic.signal_flow_graph import SFG from b_asic.simulation import Simulation from b_asic.special_operations import Input, Output if TYPE_CHECKING: from qtpy.QtWidgets import QGraphicsProxyWidget logging.basicConfig(level=logging.INFO) QCoreApplication.setOrganizationName("Linköping University") QCoreApplication.setOrganizationDomain("liu.se") QCoreApplication.setApplicationName("B-ASIC SFG GUI") QCoreApplication.setApplicationVersion(__version__) @decorate_class(handle_error) class SFGMainWindow(QMainWindow): def __init__(self): super().__init__() self._logger = logging.getLogger(__name__) self._window = self self._ui = Ui_main_window() self._ui.setupUi(self) self.setWindowIcon(QIcon("small_logo.png")) self._scene = QGraphicsScene(self._ui.splitter) self._operations_from_name: Dict[str, Operation] = {} self._zoom = 1 self._drag_operation_scenes: Dict[DragButton, "QGraphicsProxyWidget"] = {} self._drag_buttons: Dict[Operation, DragButton] = {} self._mouse_pressed = False self._mouse_dragging = False self._starting_port = None self._pressed_operations: List[DragButton] = [] self._arrow_ports: Dict[Arrow, List[Tuple[PortButton, PortButton]]] = {} self._operation_to_sfg: Dict[DragButton, SFG] = {} self._pressed_ports: List[PortButton] = [] self._sfg_dict: Dict[str, SFG] = {} self._plot: Dict[Simulation, PlotWindow] = {} self._ports: Dict[DragButton, List[PortButton]] = {} # Create Graphics View self._graphics_view = QGraphicsView(self._scene, self._ui.splitter) self._graphics_view.setRenderHint(QPainter.Antialiasing) self._graphics_view.setGeometry( self._ui.operation_box.width(), 20, self.width(), self.height() ) self._graphics_view.setDragMode(QGraphicsView.RubberBandDrag) # Create toolbar self._toolbar = self.addToolBar("Toolbar") self._toolbar.addAction(get_icon('open'), "Load SFG", self.load_work) self._toolbar.addAction(get_icon('save'), "Save SFG", self.save_work) self._toolbar.addSeparator() self._toolbar.addAction( get_icon('new-sfg'), "Create SFG", self.create_sfg_from_toolbar ) self._toolbar.addAction( get_icon('close'), "Clear workspace", self._clear_workspace ) # Create status bar self._statusbar = QStatusBar(self) self.setStatusBar(self._statusbar) # Add operations self._max_recent_files = 4 self._recent_files_actions: List[QAction] = [] self._recent_files_paths: Deque[str] = deque(maxlen=self._max_recent_files) self.add_operations_from_namespace( b_asic.core_operations, self._ui.core_operations_list ) self.add_operations_from_namespace( b_asic.special_operations, self._ui.special_operations_list ) self._shortcut_refresh_operations = QShortcut( QKeySequence("Ctrl+R"), self._ui.operation_box ) self._shortcut_refresh_operations.activated.connect( self._refresh_operations_list_from_namespace ) self._scene.selectionChanged.connect(self._select_operations) self.move_button_index = 0 self._ui.actionShowPC.triggered.connect(self._show_precedence_graph) self._ui.actionSimulateSFG.triggered.connect(self.simulate_sfg) self._ui.actionSimulateSFG.setIcon(get_icon('sim')) # About menu self._ui.faqBASIC.triggered.connect(self.display_faq_page) self._ui.faqBASIC.setShortcut(QKeySequence("Ctrl+?")) self._ui.faqBASIC.setIcon(get_icon('faq')) self._ui.aboutBASIC.triggered.connect(self.display_about_page) self._ui.aboutBASIC.setIcon(get_icon('about')) self._ui.keybindsBASIC.triggered.connect(self.display_keybindings_page) self._ui.keybindsBASIC.setIcon(get_icon('keys')) self._ui.documentationBASIC.triggered.connect(self._open_documentation) self._ui.documentationBASIC.setIcon(get_icon('docs')) # Operation lists self._ui.core_operations_list.itemClicked.connect( self._on_list_widget_item_clicked ) self._ui.special_operations_list.itemClicked.connect( self._on_list_widget_item_clicked ) self._ui.custom_operations_list.itemClicked.connect( self._on_list_widget_item_clicked ) self._ui.save_menu.triggered.connect(self.save_work) self._ui.save_menu.setIcon(get_icon('save')) self._ui.save_menu.setShortcut(QKeySequence("Ctrl+S")) self._ui.load_menu.triggered.connect(self.load_work) self._ui.load_menu.setIcon(get_icon('open')) self._ui.load_menu.setShortcut(QKeySequence("Ctrl+O")) self._ui.load_operations.triggered.connect(self.add_namespace) self._ui.load_operations.setIcon(get_icon('add-operations')) self._ui.exit_menu.triggered.connect(self.exit_app) self._ui.exit_menu.setIcon(get_icon('quit')) self._ui.select_all.triggered.connect(self._select_all) self._ui.select_all.setShortcut(QKeySequence("Ctrl+A")) self._ui.select_all.setIcon(get_icon('all')) self._ui.unselect_all.triggered.connect(self._unselect_all) self._ui.unselect_all.setIcon(get_icon('none')) self._shortcut_signal = QShortcut(QKeySequence(Qt.Key_Space), self) self._shortcut_signal.activated.connect(self._connect_callback) self._create_recent_file_actions_and_menus() # View menu # Operation names self._show_names = True self._check_show_names = QAction("&Operation names") self._check_show_names.triggered.connect(self.view_operation_names) self._check_show_names.setCheckable(True) self._check_show_names.setChecked(True) self._ui.view_menu.addAction(self._check_show_names) self._ui.view_menu.addSeparator() # Toggle toolbar self._ui.view_menu.addAction(self._toolbar.toggleViewAction()) # Toggle status bar self._statusbar_visible = QAction("&Status bar") self._statusbar_visible.setCheckable(True) self._statusbar_visible.setChecked(True) self._statusbar_visible.triggered.connect(self._toggle_statusbar) self._ui.view_menu.addAction(self._statusbar_visible) # Zoom to fit self._ui.view_menu.addSeparator() self._zoom_to_fit_action = QAction(get_icon('zoom-to-fit'), "Zoom to &fit") self._zoom_to_fit_action.triggered.connect(self._zoom_to_fit) self._ui.view_menu.addAction(self._zoom_to_fit_action) # Toggle full screen self._fullscreen_action = QAction( get_icon('full-screen'), "Toggle f&ull screen" ) self._fullscreen_action.setCheckable(True) self._fullscreen_action.triggered.connect(self._toggle_fullscreen) self._fullscreen_action.setShortcut(QKeySequence("F11")) self._ui.view_menu.addAction(self._fullscreen_action) # Non-modal dialogs self._keybindings_page = None self._about_page = None self._faq_page = None self._logger.info("Finished setting up GUI") self._logger.info( "For questions please refer to 'Ctrl+?', or visit the 'Help' " "section on the _toolbar." ) self.cursor = QCursor() def resizeEvent(self, event) -> None: ui_width = self._ui.operation_box.width() self._ui.operation_box.setGeometry(10, 10, ui_width, self.height()) self._graphics_view.setGeometry( ui_width + 20, 60, self.width() - ui_width - 20, self.height() - 30, ) super().resizeEvent(event) def wheelEvent(self, event) -> None: if event.modifiers() == Qt.KeyboardModifier.ControlModifier: old_zoom = self._zoom self._zoom += event.angleDelta().y() / 2500 self._graphics_view.scale(self._zoom, self._zoom) self._zoom = old_zoom def view_operation_names(self, event=None) -> None: self._show_names = self._check_show_names.isChecked() for operation in self._drag_operation_scenes: operation.label.setOpacity(self._show_names) operation.show_name = self._show_names def _save_work(self) -> None: if not self.sfg_widget.sfg: self.update_statusbar("No SFG selected - saving cancelled") sfg = cast(SFG, self.sfg_widget.sfg) file_dialog = QFileDialog() file_dialog.setDefaultSuffix(".py") module, accepted = file_dialog.getSaveFileName() if not accepted: return self._logger.info("Saving SFG to path: " + str(module)) operation_positions = {} for op_drag, op_scene in self._drag_operation_scenes.items(): operation_positions[op_drag.operation.graph_id] = ( int(op_scene.x()), int(op_scene.y()), op_drag.is_flipped(), ) try: with open(module, "w+") as file_obj: file_obj.write( sfg_to_python(sfg, suffix=f"positions = {operation_positions}") ) except Exception as e: self._logger.error( f"Failed to save SFG to path: {module}, with error: {e}." ) return self._logger.info("Saved SFG to path: " + str(module)) self.update_statusbar("Saved SFG to path: " + str(module)) def save_work(self, event=None) -> None: if not self._sfg_dict: self.update_statusbar("No SFG to save") return self.sfg_widget = SelectSFGWindow(self) self.sfg_widget.show() # Wait for input to dialog. self.sfg_widget.ok.connect(self._save_work) def load_work(self, event=None) -> None: module, accepted = QFileDialog().getOpenFileName() if not accepted: return self._load_from_file(module) def _load_from_file(self, module) -> None: self._logger.info("Loading SFG from path: " + str(module)) try: sfg, positions = python_to_sfg(module) except ImportError as e: self._logger.error( f"Failed to load module: {module} with the following error: {e}." ) return self._add_recent_file(module) while sfg.name in self._sfg_dict: self._logger.warning( f"Duplicate SFG with name: {sfg.name} detected. " "Please choose a new name." ) name, accepted = QInputDialog.getText( self, "Change SFG Name", "Name: ", QLineEdit.Normal ) if not accepted: return sfg.name = name self._load_sfg(sfg, positions) self._logger.info("Loaded SFG from path: " + str(module)) self.update_statusbar(f"Loaded SFG from {module}") def _load_sfg(self, sfg: SFG, positions=None) -> None: if positions is None: positions = {} for op in sfg.split(): self.add_operation( op, positions[op.graph_id][0:2] if op.graph_id in positions else None, positions[op.graph_id][-1] if op.graph_id in positions else None, ) def connect_ports(ports: Sequence[InputPort]): for port in ports: for signal in port.signals: sources = [ source for source in self._drag_buttons[ signal.source_operation ].port_list if source.port is signal.source ] destinations = [ destination for destination in self._drag_buttons[ signal.destination.operation ].port_list if destination.port is signal.destination ] if sources and destinations: self._connect_button(sources[0], destinations[0]) for pressed_port in self._pressed_ports: pressed_port.select_port() for op in sfg.split(): connect_ports(op.inputs) for op in sfg.split(): self._drag_buttons[op].setToolTip(sfg.name) self._operation_to_sfg[self._drag_buttons[op]] = sfg self._sfg_dict[sfg.name] = sfg self.update() def _create_recent_file_actions_and_menus(self): for i in range(self._max_recent_files): recent_file_action = QAction(self._ui.recent_sfg) recent_file_action.setVisible(False) recent_file_action.triggered.connect( lambda b=0, x=recent_file_action: self._open_recent_file(x) ) self._recent_files_actions.append(recent_file_action) self._ui.recent_sfg.addAction(recent_file_action) self._update_recent_file_list() def _toggle_fullscreen(self, event=None): """Callback for toggling full screen mode.""" if self.isFullScreen(): self.showNormal() self._fullscreen_action.setIcon(get_icon('full-screen')) else: self.showFullScreen() self._fullscreen_action.setIcon(get_icon('full-screen-exit')) def _update_recent_file_list(self): settings = QSettings() rfp = cast(deque, settings.value("SFG/recentFiles")) # print(rfp) if rfp: dequelen = len(rfp) if dequelen > 0: for i in range(dequelen): action = self._recent_files_actions[i] action.setText(rfp[i]) action.setData(QFileInfo(rfp[i])) action.setVisible(True) for i in range(dequelen, self._max_recent_files): self._recent_files_actions[i].setVisible(False) def _open_recent_file(self, action): self._load_from_file(action.data().filePath()) def _add_recent_file(self, module): settings = QSettings() rfp = cast(deque, settings.value("SFG/recentFiles")) if rfp: if module not in rfp: rfp.append(module) else: rfp = deque(maxlen=self._max_recent_files) rfp.append(module) settings.setValue("SFG/recentFiles", rfp) self._update_recent_file_list() def exit_app(self, event=None) -> None: """Exit the application.""" self._logger.info("Exiting the application.") QApplication.quit() def update_statusbar(self, msg: str) -> None: """ Write *msg* to the statusbar with temporarily policy. Parameters ---------- msg : str The message to write. """ self._statusbar.showMessage(msg) def _clear_workspace(self) -> None: self._logger.info("Clearing workspace from operations and SFGs.") self._pressed_operations.clear() self._pressed_ports.clear() self._drag_buttons.clear() self._drag_operation_scenes.clear() self._arrow_ports.clear() self._ports.clear() self._sfg_dict.clear() self._scene.clear() self._logger.info("Workspace cleared.") self.update_statusbar("Workspace cleared.") def create_sfg_from_toolbar(self) -> None: """Callback to create an SFG.""" inputs = [] outputs = [] for pressed_op in self._pressed_operations: if isinstance(pressed_op.operation, Input): inputs.append(pressed_op.operation) elif isinstance(pressed_op.operation, Output): outputs.append(pressed_op.operation) name, accepted = QInputDialog.getText( self, "Create SFG", "Name: ", QLineEdit.Normal ) if not accepted: return if not name: self._logger.warning("Failed to initialize SFG with empty name.") return self._logger.info("Creating SFG with name: %s from selected operations." % name) sfg = SFG(inputs=inputs, outputs=outputs, name=name) self._logger.info("Created SFG with name: %s from selected operations." % name) self.update_statusbar(f"Created SFG: {name}") def check_equality(signal: Signal, signal_2: Signal) -> bool: source_operation = cast(Operation, signal.source_operation) source_operation2 = cast(Operation, signal_2.source_operation) dest_operation = cast(Operation, signal.destination_operation) dest_operation2 = cast(Operation, signal_2.destination_operation) if not ( source_operation.type_name() == source_operation2.type_name() and dest_operation.type_name() == dest_operation2.type_name() ): return False if ( hasattr(source_operation, "value") and hasattr(source_operation2, "value") and hasattr(dest_operation, "value") and hasattr(dest_operation2, "value") ): if not ( source_operation.value == source_operation2.value and dest_operation.value == dest_operation2.value ): return False if ( hasattr(source_operation, "name") and hasattr(source_operation2, "name") and hasattr(dest_operation, "name") and hasattr(dest_operation2, "name") ): if not ( source_operation.name == source_operation2.name and dest_operation.name == dest_operation2.name ): return False try: signal_source_index = [ source_operation.outputs.index(port) for port in source_operation.outputs if signal in port.signals ] signal_2_source_index = [ source_operation2.outputs.index(port) for port in source_operation2.outputs if signal_2 in port.signals ] except ValueError: return False # Signal output connections not matching try: signal_destination_index = [ dest_operation.inputs.index(port) for port in dest_operation.inputs if signal in port.signals ] signal_2_destination_index = [ dest_operation2.inputs.index(port) for port in dest_operation2.inputs if signal_2 in port.signals ] except ValueError: return False # Signal input connections not matching return ( signal_source_index == signal_2_source_index and signal_destination_index == signal_2_destination_index ) for _pressed_op in self._pressed_operations: for operation in sfg.operations: for input_ in operation.inputs: for signal in input_.signals: for arrow in self._arrow_ports: if check_equality(arrow.signal, signal): arrow.set_source_operation(signal.source_operation) arrow.set_destination_operation( signal.destination_operation ) for output_ in operation.outputs: for signal in output_.signals: for arrow in self._arrow_ports: if check_equality(arrow.signal, signal): arrow.set_source_operation(signal.source_operation) arrow.set_destination_operation( signal.destination_operation ) for pressed_op in self._pressed_operations: pressed_op.setToolTip(sfg.name) self._operation_to_sfg[pressed_op] = sfg self._sfg_dict[sfg.name] = sfg def _show_precedence_graph(self, event=None) -> None: """Callback for showing precedence graph.""" if not self._sfg_dict: self.update_statusbar("No SFG to show") return self._precedence_graph_dialog = PrecedenceGraphWindow(self) self._precedence_graph_dialog.add_sfg_to_dialog() self._precedence_graph_dialog.show() def _toggle_statusbar(self, event=None) -> None: """Callback for toggling the status bar.""" self._statusbar.setVisible(self._statusbar_visible.isChecked()) def get_operations_from_namespace(self, namespace: ModuleType) -> List[str]: """ Return a list of all operations defined in a namespace (module). Parameters ---------- namespace : module A loaded Python module containing operations. Returns ------- list A list of names of all the operations in the module. """ self._logger.info( "Fetching operations from namespace: " + str(namespace.__name__) ) return [ comp for comp in dir(namespace) if hasattr(getattr(namespace, comp), "type_name") ] def add_operations_from_namespace( self, namespace: ModuleType, list_widget: QListWidget ) -> None: """ Add operations from namespace (module) to a list widget. Parameters ---------- namespace : module A loaded Python module containing operations. list_widget : QListWidget The widget to add operations to. """ for attr_name in self.get_operations_from_namespace(namespace): attr = getattr(namespace, attr_name) try: attr.type_name() item = QListWidgetItem(attr_name) list_widget.addItem(item) self._operations_from_name[attr_name] = attr except NotImplementedError: pass self._logger.info("Added operations from namespace: " + str(namespace.__name__)) def add_namespace(self, event=None) -> None: """Callback for adding namespace.""" module, accepted = QFileDialog().getOpenFileName() if not accepted: return self._add_namespace(module) def _add_namespace(self, module: str): spec = importlib.util.spec_from_file_location( f"{QFileInfo(module).fileName()}", module ) namespace = importlib.util.module_from_spec(spec) spec.loader.exec_module(namespace) self.add_operations_from_namespace(namespace, self._ui.custom_operations_list) def _update(self): self._scene.update() self._graphics_view.update() def add_operation( self, op: Operation, position: Optional[Tuple[float, float]] = None, is_flipped: bool = False, ) -> None: """ Add operation to GUI. Parameters ---------- op : Operation The operation to add. position : (float, float), optional (x, y)-position for operation. is_flipped : bool, default: False Whether the operation is flipped. """ try: if op in self._drag_buttons: self._logger.warning("Multiple instances of operation with same name") return attr_button = DragButton(op, True, window=self) if position is None: attr_button.move(GRID * 3, GRID * 2) else: attr_button.move(*position) max_ports = max(op.input_count, op.output_count) button_height = max( MINBUTTONSIZE, max_ports * PORTHEIGHT + (max_ports - 1) * GAP ) attr_button.setFixedSize(MINBUTTONSIZE, button_height) attr_button.setStyleSheet( "background-color: white; border-style: solid;" "border-color: black; border-width: 2px" ) attr_button.add_ports() self._ports[attr_button] = attr_button.port_list icon_path = os.path.join( os.path.dirname(__file__), "operation_icons", f"{op.type_name().lower()}.png", ) if not os.path.exists(icon_path): icon_path = os.path.join( os.path.dirname(__file__), "operation_icons", "custom_operation.png", ) attr_button.setIcon(QIcon(icon_path)) attr_button.setIconSize(QSize(MINBUTTONSIZE, MINBUTTONSIZE)) attr_button.setToolTip("No SFG") attr_button.setStyleSheet( "QToolTip { background-color: white; color: black }" ) attr_button.setParent(None) attr_button_scene = self._scene.addWidget(attr_button) if position is None: attr_button_scene.moveBy( int(self._scene.width() / 4), int(self._scene.height() / 4) ) attr_button_scene.setFlag(QGraphicsItem.ItemIsSelectable, True) operation_label = QGraphicsTextItem(op.name, attr_button_scene) if not self._show_names: operation_label.setOpacity(0) operation_label.setTransformOriginPoint( operation_label.boundingRect().center() ) operation_label.moveBy(10, -20) attr_button.add_label(operation_label) if isinstance(is_flipped, bool): if is_flipped: attr_button._flip() self._drag_buttons[op] = attr_button self._drag_operation_scenes[attr_button] = attr_button_scene except Exception as e: self._logger.error( "Unexpected error occurred while creating operation: " + str(e) ) def _create_operation_item(self, item) -> None: self._logger.info("Creating operation of type: %s" % str(item.text())) try: attr_operation = self._operations_from_name[item.text()]() self.add_operation(attr_operation) self.update_statusbar(f"{item.text()} added.") except Exception as e: self._logger.error( "Unexpected error occurred while creating operation: " + str(e) ) def _refresh_operations_list_from_namespace(self) -> None: self._logger.info("Refreshing operation list.") self._ui.core_operations_list.clear() self._ui.special_operations_list.clear() self.add_operations_from_namespace( b_asic.core_operations, self._ui.core_operations_list ) self.add_operations_from_namespace( b_asic.special_operations, self._ui.special_operations_list ) self._logger.info("Finished refreshing operation list.") def _on_list_widget_item_clicked(self, item) -> None: self._create_operation_item(item) def keyPressEvent(self, event) -> None: if event.key() == Qt.Key.Key_Delete: for pressed_op in self._pressed_operations: pressed_op.remove() self.move_button_index -= 1 self._pressed_operations.clear() super().keyPressEvent(event) def _connect_callback(self, *event) -> None: """Callback for connecting operation buttons.""" if len(self._pressed_ports) < 2: self._logger.warning( "Cannot connect less than two ports. Please select at least two." ) return pressed_op_inports = [ pressed for pressed in self._pressed_ports if isinstance(pressed.port, InputPort) ] pressed_op_outports = [ pressed for pressed in self._pressed_ports if isinstance(pressed.port, OutputPort) ] if len(pressed_op_outports) != 1: raise ValueError("Exactly one output port must be selected!") pressed_op_outport = pressed_op_outports[0] for pressed_op_input_port in pressed_op_inports: self._connect_button(pressed_op_outport, pressed_op_input_port) for port in self._pressed_ports: port.select_port() def _connect_button(self, source: PortButton, destination: PortButton) -> None: """ Connect two PortButtons with an Arrow. Parameters ---------- source : PortButton The PortButton to start the signal at. destination : PortButton The PortButton to end the signal at. Returns ------- None. """ signal_exists = ( signal for signal in source.port.signals if signal.destination is destination.port ) self._logger.info( "Connecting: %s -> %s." % ( source.operation.type_name(), destination.operation.type_name(), ) ) try: arrow = Arrow(source, destination, self, signal=next(signal_exists)) except StopIteration: arrow = Arrow(source, destination, self) if arrow not in self._arrow_ports: self._arrow_ports[arrow] = [] self._arrow_ports[arrow].append((source, destination)) self._scene.addItem(arrow) self.update() def paintEvent(self, event) -> None: for arrow in self._arrow_ports: arrow.update_arrow() def _select_operations(self) -> None: """Select an operation button.""" selected = [button.widget() for button in self._scene.selectedItems()] for button in selected: button._toggle_button(pressed=False) for button in self._pressed_operations: if button not in selected: button._toggle_button(pressed=True) self._pressed_operations = selected def _select_all(self, event=None) -> None: """Callback for selecting all operation buttons.""" if not self._drag_buttons: self.update_statusbar("No operations to select") return for operation in self._drag_buttons.values(): operation._toggle_button(pressed=False) self.update_statusbar("Selected all operations") def _unselect_all(self, event=None) -> None: """Callback for unselecting all operation buttons.""" if not self._drag_buttons: self.update_statusbar("No operations to unselect") return for operation in self._drag_buttons.values(): operation._toggle_button(pressed=True) self.update_statusbar("Unselected all operations") def _zoom_to_fit(self, event=None): """Callback for zoom to fit SFGs in window.""" self._graphics_view.fitInView( self._scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio ) def _simulate_sfg(self) -> None: """Callback for simulating SFGs in separate threads.""" self._thread = dict() self._sim_worker = dict() for sfg, properties in self._simulation_dialog._properties.items(): self._logger.info("Simulating SFG with name: %s" % str(sfg.name)) self._sim_worker[sfg] = SimulationWorker(sfg, properties) self._thread[sfg] = QThread() self._sim_worker[sfg].moveToThread(self._thread[sfg]) self._thread[sfg].started.connect(self._sim_worker[sfg].start_simulation) self._sim_worker[sfg].finished.connect(self._thread[sfg].quit) self._sim_worker[sfg].finished.connect(self._show_plot_window) self._sim_worker[sfg].finished.connect(self._sim_worker[sfg].deleteLater) self._thread[sfg].finished.connect(self._thread[sfg].deleteLater) self._thread[sfg].start() def _show_plot_window(self, sim: Simulation): """Callback for displaying simulation results window.""" self._plot[sim] = PlotWindow(sim.results, sfg_name=sim.sfg.name) self._plot[sim].show() def simulate_sfg(self, event=None) -> None: """Callback for showing simulation dialog.""" if not self._sfg_dict: self.update_statusbar("No SFG to simulate") return self._simulation_dialog = SimulateSFGWindow(self) for _, sfg in self._sfg_dict.items(): self._simulation_dialog.add_sfg_to_dialog(sfg) self._simulation_dialog.show() # Wait for input to dialog. # Kinda buggy because of the separate window in the same thread. self._simulation_dialog.simulate.connect(self._simulate_sfg) @Slot() def _open_documentation(self, event=None) -> None: """Callback to open documentation web page.""" webbrowser.open_new_tab("https://da.gitlab-pages.liu.se/B-ASIC/") def display_faq_page(self, event=None) -> None: """Callback for displaying FAQ dialog.""" if self._faq_page is None: self._faq_page = FaqWindow(self) self._faq_page._scroll_area.show() def display_about_page(self, event=None) -> None: """Callback for displaying about dialog.""" if self._about_page is None: self._about_page = AboutWindow(self) self._about_page.show() def display_keybindings_page(self, event=None) -> None: """Callback for displaying keybindings dialog.""" if self._keybindings_page is None: self._keybindings_page = KeybindingsWindow(self) self._keybindings_page.show() def start_editor(sfg: Optional[SFG] = None) -> Dict[str, SFG]: """ Start the SFG editor. Parameters ---------- sfg : SFG, optional The SFG to start the editor with. Returns ------- dict All SFGs currently in the editor. """ if not QApplication.instance(): app = QApplication(sys.argv) else: app = QApplication.instance() window = SFGMainWindow() if sfg: window._load_sfg(sfg) window.show() app.exec_() return window._sfg_dict if __name__ == "__main__": start_editor()