Skip to content
Snippets Groups Projects
main_window.py 29.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • """
    B-ASIC Signal Flow Graph Editor Module.
    
    This file opens the main SFG editor window of the GUI for B-ASIC when run.
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    import os
    
    Olle Hansson's avatar
    Olle Hansson committed
    from collections import deque
    
    from typing import TYPE_CHECKING, Deque, Dict, List, Optional, Sequence, Tuple, cast
    
    from qtpy.QtCore import QCoreApplication, QFileInfo, QSettings, QSize, Qt, QThread
    
    Olle Hansson's avatar
    Olle Hansson committed
    from qtpy.QtGui import QCursor, QIcon, QKeySequence, QPainter
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    from qtpy.QtWidgets import (
        QAction,
        QApplication,
        QFileDialog,
    
        QGraphicsItem,
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        QGraphicsScene,
        QGraphicsTextItem,
        QGraphicsView,
        QInputDialog,
        QLineEdit,
        QListWidgetItem,
        QMainWindow,
        QShortcut,
    )
    
    
    import b_asic.core_operations
    import b_asic.special_operations
    
    Olle Hansson's avatar
    Olle Hansson committed
    from b_asic._version import __version__
    
    from b_asic.GUI._preferences import GAP, GRID, MINBUTTONSIZE, PORTHEIGHT
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    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
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    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
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    from b_asic.gui_utils.icons import get_icon
    
    from b_asic.gui_utils.plot_window import PlotWindow
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    from b_asic.operation import Operation
    
    from b_asic.port import InputPort, OutputPort
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    from b_asic.save_load_structure import python_to_sfg, sfg_to_python
    
    from b_asic.signal import Signal
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
    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
    
    
    Olle Hansson's avatar
    Olle Hansson committed
    QCoreApplication.setOrganizationName("Linköping University")
    QCoreApplication.setOrganizationDomain("liu.se")
    QCoreApplication.setApplicationName("B-ASIC SFG GUI")
    QCoreApplication.setApplicationVersion(__version__)
    
    
    class SFGMainWindow(QMainWindow):
    
            self._ui = Ui_main_window()
            self._ui.setupUi(self)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            self.setWindowIcon(QIcon("small_logo.png"))
    
            self._scene = QGraphicsScene(self)
    
            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._logger = logging.getLogger(__name__)
    
            self._plot: Dict[Simulation, PlotWindow] = {}
            self._ports: Dict[DragButton, List[PortButton]] = {}
    
    
            # Create Graphics View
    
            self._graphics_view = QGraphicsView(self._scene, self)
            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")
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            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
            )
    
    
            # 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)
    
    Olle Hansson's avatar
    Olle Hansson committed
    
    
                b_asic.core_operations, self._ui.core_operations_list
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
                b_asic.special_operations, self._ui.special_operations_list
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
            self._shortcut_refresh_operations = QShortcut(
                QKeySequence("Ctrl+R"), self._ui.operation_box
            )
            self._shortcut_refresh_operations.activated.connect(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                self._refresh_operations_list_from_namespace
            )
    
            self._scene.selectionChanged.connect(self._select_operations)
    
            self._check_show_names = QAction("Show 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.actionShowPC.triggered.connect(self._show_precedence_graph)
            self._ui.actionSimulateSFG.triggered.connect(self.simulate_sfg)
            self._ui.faqBASIC.triggered.connect(self.display_faq_page)
            self._ui.aboutBASIC.triggered.connect(self.display_about_page)
            self._ui.keybindsBASIC.triggered.connect(self.display_keybindings_page)
            self._ui.core_operations_list.itemClicked.connect(
    
                self._on_list_widget_item_clicked
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
            self._ui.special_operations_list.itemClicked.connect(
    
                self._on_list_widget_item_clicked
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
            self._ui.custom_operations_list.itemClicked.connect(
    
                self._on_list_widget_item_clicked
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
            self._ui.save_menu.triggered.connect(self.save_work)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            self._ui.save_menu.setIcon(get_icon('save'))
    
            self._ui.load_menu.triggered.connect(self.load_work)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            self._ui.load_menu.setIcon(get_icon('open'))
    
            self._ui.load_operations.triggered.connect(self.add_namespace)
            self._ui.exit_menu.triggered.connect(self.exit_app)
            self._shortcut_open = QShortcut(QKeySequence("Ctrl+O"), self)
            self._shortcut_open.activated.connect(self.load_work)
            self._shortcut_save = QShortcut(QKeySequence("Ctrl+S"), self)
            self._shortcut_save.activated.connect(self.save_work)
            self._shortcut_help = QShortcut(QKeySequence("Ctrl+?"), self)
            self._shortcut_help.activated.connect(self.display_faq_page)
            self._shortcut_signal = QShortcut(QKeySequence(Qt.Key_Space), self)
            self._shortcut_signal.activated.connect(self._connect_callback)
            self._create_recent_file_actions_and_menus()
    
            self._keybindings_page = None
            self._about_page = None
            self._faq_page = None
    
    
            self._logger.info("Finished setting up GUI")
            self._logger.info(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                "For questions please refer to 'Ctrl+?', or visit the 'Help' "
    
                "section on the _toolbar."
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
    Olle Hansson's avatar
    Olle Hansson committed
            self.cursor = QCursor()
    
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        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,
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                60,
    
                self.width() - ui_width - 20,
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                self.height() - 30,
            )
    
            super().resizeEvent(event)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        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
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def view_operation_names(self) -> None:
    
            if self._check_show_names.isChecked():
    
                self._show_names = False
    
            for operation in self._drag_operation_scenes:
    
                operation.label.setOpacity(self._show_names)
                operation.show_name = self._show_names
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def _save_work(self) -> None:
    
            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))
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            operation_positions = {}
    
            for op_drag, op_scene in self._drag_operation_scenes.items():
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                operation_positions[op_drag.operation.graph_id] = (
    
                    int(op_scene.x()),
                    int(op_scene.y()),
    
                    op_drag.is_flipped(),
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    file_obj.write(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                        sfg_to_python(sfg, suffix=f"positions = {operation_positions}")
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    )
    
                self._logger.error(
                    f"Failed to save SFG to path: {module}, with error: {e}."
                )
    
            self._logger.info("Saved SFG to path: " + str(module))
    
        def save_work(self, event=None) -> None:
    
            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(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    f"Failed to load module: {module} with the following error: {e}."
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
            self._add_recent_file(module)
    
            while sfg.name in self._sfg_dict:
                self._logger.warning(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    f"Duplicate SFG with name: {sfg.name} detected. "
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    "Please choose a new name."
                )
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    self, "Change SFG Name", "Name: ", QLineEdit.Normal
                )
    
            self._load_sfg(sfg, positions)
    
            self._logger.info("Loaded SFG from path: " + str(module))
    
        def _load_sfg(self, sfg: SFG, positions=None) -> None:
    
            if positions is None:
                positions = {}
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                self.add_operation(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    op,
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    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,
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
            def connect_ports(ports: Sequence[InputPort]):
    
                for port in ports:
                    for signal in port.signals:
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                            source
    
                            for source in self._drag_buttons[
                                signal.source_operation
                            ].port_list
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                            if source.port is signal.source
                        ]
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                            destination
    
                            for destination in self._drag_buttons[
                                signal.destination.operation
                            ].port_list
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                            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
    
        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)
    
    Olle Hansson's avatar
    Olle Hansson committed
                )
    
                self._recent_files_actions.append(recent_file_action)
                self._ui.recent_sfg.addAction(recent_file_action)
    
    Olle Hansson's avatar
    Olle Hansson committed
    
    
            self._update_recent_file_list()
    
    Olle Hansson's avatar
    Olle Hansson committed
    
    
        def _update_recent_file_list(self):
    
    Olle Hansson's avatar
    Olle Hansson committed
            settings = QSettings()
    
    
            rfp = cast(deque, settings.value("SFG/recentFiles"))
    
    Olle Hansson's avatar
    Olle Hansson committed
    
            # 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]))
    
    Olle Hansson's avatar
    Olle Hansson committed
                        action.setVisible(True)
    
    
                    for i in range(dequelen, self._max_recent_files):
    
                        self._recent_files_actions[i].setVisible(False)
    
    Olle Hansson's avatar
    Olle Hansson committed
    
    
        def _open_recent_file(self, action):
    
    Olle Hansson's avatar
    Olle Hansson committed
            self._load_from_file(action.data().filePath())
    
    
        def _add_recent_file(self, module):
    
    Olle Hansson's avatar
    Olle Hansson committed
            settings = QSettings()
    
    
            rfp = cast(deque, settings.value("SFG/recentFiles"))
    
    Olle Hansson's avatar
    Olle Hansson committed
    
            if rfp:
    
                if module not in rfp:
                    rfp.append(module)
    
    Olle Hansson's avatar
    Olle Hansson committed
            else:
    
                rfp = deque(maxlen=self._max_recent_files)
    
                rfp.append(module)
    
    Olle Hansson's avatar
    Olle Hansson committed
    
            settings.setValue("SFG/recentFiles", rfp)
    
    
            self._update_recent_file_list()
    
    Olle Hansson's avatar
    Olle Hansson committed
    
    
        def exit_app(self) -> None:
    
            self._logger.info("Exiting the application.")
    
        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.")
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def create_sfg_from_toolbar(self) -> None:
    
            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)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                self, "Create SFG", "Name: ", QLineEdit.Normal
            )
    
                self._logger.warning("Failed to initialize SFG with empty name.")
    
            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)
    
            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)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                if not (
    
                    source_operation.type_name() == source_operation2.type_name()
                    and dest_operation.type_name() == dest_operation2.type_name()
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                ):
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                if (
    
                    hasattr(source_operation, "value")
                    and hasattr(source_operation2, "value")
                    and hasattr(dest_operation, "value")
                    and hasattr(dest_operation2, "value")
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                ):
                    if not (
    
                        source_operation.value == source_operation2.value
                        and dest_operation.value == dest_operation2.value
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    ):
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                if (
    
                    hasattr(source_operation, "name")
                    and hasattr(source_operation2, "name")
                    and hasattr(dest_operation, "name")
                    and hasattr(dest_operation2, "name")
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                ):
                    if not (
    
                        source_operation.name == source_operation2.name
                        and dest_operation.name == dest_operation2.name
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    ):
    
                    signal_source_index = [
                        source_operation.outputs.index(port)
                        for port in source_operation.outputs
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                        if signal in port.signals
                    ]
    
                    signal_2_source_index = [
                        source_operation2.outputs.index(port)
                        for port in source_operation2.outputs
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                        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
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                        if signal in port.signals
                    ]
    
                    signal_2_destination_index = [
                        dest_operation2.inputs.index(port)
                        for port in dest_operation2.inputs
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                        if signal_2 in port.signals
                    ]
    
                except ValueError:
                    return False  # Signal input connections not matching
    
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                return (
    
                    signal_source_index == signal_2_source_index
                    and signal_destination_index == signal_2_destination_index
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
            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
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                                    )
    
    
                    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
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                                    )
    
            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
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def _show_precedence_graph(self, event=None) -> None:
    
            self._precedence_graph_dialog = PrecedenceGraphWindow(self)
    
            self._precedence_graph_dialog.add_sfg_to_dialog()
            self._precedence_graph_dialog.show()
    
        def get_operations_from_namespace(self, namespace) -> List[str]:
    
            self._logger.info(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                "Fetching operations from namespace: " + str(namespace.__name__)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
            return [
                comp
                for comp in dir(namespace)
                if hasattr(getattr(namespace, comp), "type_name")
            ]
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def add_operations_from_namespace(self, namespace, _list) -> None:
    
            for attr_name in self.get_operations_from_namespace(namespace):
                attr = getattr(namespace, attr_name)
                try:
                    attr.type_name()
                    item = QListWidgetItem(attr_name)
                    _list.addItem(item)
                    self._operations_from_name[attr_name] = attr
                except NotImplementedError:
                    pass
    
    
            self._logger.info("Added operations from namespace: " + str(namespace.__name__))
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def add_namespace(self, event=None) -> None:
    
            module, accepted = QFileDialog().getOpenFileName()
            if not accepted:
                return
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            self._add_namespace(module)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def _add_namespace(self, module):
    
            spec = importlib.util.spec_from_file_location(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                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()
    
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def add_operation(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            self,
            op: Operation,
            position: Optional[Tuple[float, float]] = None,
            is_flipped: bool = False,
        ) -> None:
            """
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            Add operation to GUI.
    
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            Parameters
            ----------
            op : Operation
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                The operation to add.
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            position : (float, float), optional
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                (x, y)-position for operation.
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            is_flipped : bool, default: False
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                Whether the operation is flipped.
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            """
    
                if op in self._drag_buttons:
                    self._logger.warning("Multiple instances of operation with same name")
    
                attr_button = DragButton(op, True, window=self)
    
                    attr_button.move(GRID * 3, GRID * 2)
    
                max_ports = max(op.input_count, op.output_count)
                button_height = max(
                    MINBUTTONSIZE, max_ports * PORTHEIGHT + (max_ports - 1) * GAP
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
    
                attr_button.setFixedSize(MINBUTTONSIZE, button_height)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                attr_button.setStyleSheet(
                    "background-color: white; border-style: solid;"
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    "border-color: black; border-width: 2px"
                )
    
                attr_button.add_ports()
    
                self._ports[attr_button] = attr_button.port_list
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                icon_path = os.path.join(
                    os.path.dirname(__file__),
                    "operation_icons",
                    f"{op.type_name().lower()}.png",
                )
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                if not os.path.exists(icon_path):
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    icon_path = os.path.join(
                        os.path.dirname(__file__),
                        "operation_icons",
                        "custom_operation.png",
                    )
    
                attr_button.setIconSize(QSize(MINBUTTONSIZE, MINBUTTONSIZE))
    
                attr_button.setToolTip("No SFG")
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                attr_button.setStyleSheet(
    
                    "QToolTip { background-color: white; color: black }"
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
                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)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    )
    
                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(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    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
    
                self._logger.error(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    "Unexpected error occurred while creating operation: " + str(e)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def _create_operation_item(self, item) -> None:
    
            self._logger.info("Creating operation of type: %s" % str(item.text()))
    
                attr_operation = self._operations_from_name[item.text()]()
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                self.add_operation(attr_operation)
    
                self._logger.error(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    "Unexpected error occurred while creating operation: " + str(e)
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        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()
    
                b_asic.core_operations, self._ui.core_operations_list
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
                b_asic.special_operations, self._ui.special_operations_list
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
            self._logger.info("Finished refreshing operation list.")
    
        def _on_list_widget_item_clicked(self, item) -> None:
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        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()
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        def _connect_callback(self, *event) -> None:
    
            if len(self._pressed_ports) < 2:
                self._logger.warning(
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                    "Cannot connect less than two ports. Please select at least two."
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                )
    
            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_inport in pressed_op_inports:
                self._connect_button(pressed_op_outport, pressed_op_inport)
    
            for port in self._pressed_ports:
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
        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.
    
            """
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
                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(),
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            )
    
                arrow = Arrow(source, destination, self, signal=next(signal_exists))
    
                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)
    
        def paintEvent(self, event) -> None:
    
            for arrow in self._arrow_ports:
                arrow.update_arrow()
    
        def _select_operations(self) -> None:
    
            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 _simulate_sfg(self) -> None:
    
            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):
    
            self._plot[sim] = PlotWindow(sim.results, sfg_name=sim.sfg.name)
    
            self._plot[sim].show()
    
        def simulate_sfg(self, event=None) -> None:
            self._simulation_dialog = SimulateSFGWindow(self)
    
            for _, sfg in self._sfg_dict.items():
    
                self._simulation_dialog.add_sfg_to_dialog(sfg)
    
            self._simulation_dialog.show()
    
    Oscar Gustafsson's avatar
    Oscar Gustafsson committed
            # Wait for input to dialog.
            # Kinda buggy because of the separate window in the same thread.
    
            self._simulation_dialog.simulate.connect(self._simulate_sfg)
    
        def display_faq_page(self, event=None) -> None:
    
            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:
    
            if self._about_page is None:
                self._about_page = AboutWindow(self)
    
            self._about_page.show()
    
        def display_keybindings_page(self, event=None) -> None:
            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]:
    
        window = SFGMainWindow()
    
        if sfg:
            window._load_sfg(sfg)
    
        app.exec_()
        return window._sfg_dict