diff --git a/src/simudator/gui/cpu_graphics_scene.py b/src/simudator/gui/cpu_graphics_scene.py index 47292c4ec7552e6f75e0243cbb44009a5bdee77b..a64e417b1dfa6a4b9d834bb031373a2f9364a3f1 100644 --- a/src/simudator/gui/cpu_graphics_scene.py +++ b/src/simudator/gui/cpu_graphics_scene.py @@ -1,8 +1,22 @@ -from qtpy.QtWidgets import QGraphicsItem, QGraphicsScene +import json +from json.decoder import JSONDecodeError + +from qtpy.QtCore import QPointF +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import ( + QErrorMessage, + QFileDialog, + QGraphicsItem, + QGraphicsScene, + QMessageBox, +) from simudator.core.processor import Processor from simudator.gui.color_scheme import ColorScheme from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem +from simudator.gui.orientation import Orientation +from simudator.gui.port_graphics_item import PortGraphicsItem from simudator.gui.signal_graphics_item import SignalGraphicsItem @@ -10,69 +24,78 @@ class CpuGraphicsScene(QGraphicsScene): """ This class creates the graphics scene for visualising the processor. It takes each module representation as a ModuleGraphicsItem - and handels mouse inputs for interacting with these graphicsitems. + and handles mouse inputs for interacting with these graphicsitems. Can create a default layout on creation and can save/load new layouts. """ MODULE_SPACEING = 100 - def __init__(self, cpu: Processor): + def __init__(self): super().__init__() - self.cpu = cpu - self.module_graphics_items = dict() - self.signal_graphics_items = [] + self._module_graphics_items = dict() + self._signal_graphics_items = [] + self._error_msg_box = QErrorMessage() self.setBackgroundBrush(ColorScheme.Window) - def resetSignals(self) -> None: + def reset_signals(self) -> None: """ - Resets all graphical signals to their default visual representation + Reset all graphical signals to their default visual representation when initialised. """ - for graphics_Signal in self.signal_graphics_items: - graphics_Signal.reset() + for graphics_signal in self._signal_graphics_items: + graphics_signal.reset() - def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None: - """ - Takes a ModuleGraphicsItem and adds it to the scene. - Will give it a new position according to the deafult layout. + def add_module(self, item: ModuleGraphicsItem) -> None: """ - self.module_graphics_items[graphics_item.name] = graphics_item - self.placeModuleGraphicsItemDefault(graphics_item) + Add a graphical module item to the processor scene at a position + according to the default layout. - def placeModuleGraphicsItemDefault(self, graphics_item: ModuleGraphicsItem) -> None: - """ - Places a module graphics items at a position where the x value - is equal to the y value. Used to place all graphics items in a - diagonal. + Parameters + ---------- + item : ModuleGraphicsItem + Graphical module item to add to the scene. """ - placement = len(self.module_graphics_items) * self.MODULE_SPACEING - graphics_item.setPos(placement, placement) - self.addItem(graphics_item) + self._module_graphics_items[item.name] = item + self._place_module_default(item) - def replaceModuleGraphicsItem( - self, graphics_item: ModuleGraphicsItem, pos_x: int, pos_y: int - ) -> None: + def _place_module_default(self, item: ModuleGraphicsItem) -> None: """ - Changes the postions of an existing modules graphics item. + Place a graphical module item at a position according to the default + layout, i.e. placing all module items along a diagonal. + + Parameters + ---------- + item : ModuleGraphicsItem """ - graphics_item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING) + placement = len(self._module_graphics_items) * self.MODULE_SPACEING + item.setPos(placement, placement) + self.addItem(item) - def updateGraphicsItems(self): + def move_module_to(self, item: ModuleGraphicsItem, pos_x: int, pos_y: int) -> None: """ - Used to update graphicsitems when modules in the processor has chnaged values. + Place an existing graphical module item at some position. + + Parameters + ---------- + item : ModuleGraphicsItem + Graphical module item to place at a position. + pos_x : int + Position on x-axis to place the module item at. + pos_y : int + Position on y-axis to place the module item at. """ - for graphics_item in self.module_graphics_items.values(): - graphics_item.update() + item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING) - def addAllSignals(self) -> None: + def add_all_signals(self) -> None: """ - Instantiates signals between all matching ports of all modules. + Instantiate graphical signals between all matching ports of all + graphical modules. """ # Map which ports that are connected using their signals' names # (Every port keeps a reference to a simulation signal which has # a name) signal_to_ports = {} - for module_w in self.module_graphics_items.values(): + for module_w in self._module_graphics_items.values(): ports = module_w.getPorts() for port in ports: s_name = port.getSignalName() @@ -88,96 +111,317 @@ class CpuGraphicsScene(QGraphicsScene): port_1 = module_graphics_items[0] port_2 = module_graphics_items[1] signal_w = SignalGraphicsItem(port_1, port_2) - self.signal_graphics_items.append(signal_w) + self._signal_graphics_items.append(signal_w) self.addItem(signal_w) port_1.moved.connect(signal_w.move) port_2.moved.connect(signal_w.move) port_1.toggled.connect(signal_w.toggleVisibility) port_2.toggled.connect(signal_w.toggleVisibility) - def getModulesGraphicsItems(self) -> list[ModuleGraphicsItem]: - return list(self.module_graphics_items.values()) + def get_modules(self) -> list[ModuleGraphicsItem]: + """ + Get a list of all graphical module items in the scene. - def moduleGraphicsItemsDict(self) -> dict[str, ModuleGraphicsItem]: - return self.module_graphics_items + Returns + ------- + list[ModuleGraphicsItem] + List of graphical module items in the scene. + """ + return list(self._module_graphics_items.values()) - def getSignalGraphicsItems(self) -> list[SignalGraphicsItem]: - return self.signal_graphics_items + def get_signals(self) -> list[SignalGraphicsItem]: + """ + Get a list of all graphical signal items in the scene. + + Returns + ------- + list[SignalGraphicsItem] + List of graphical signal items in the scene. + """ + return self._signal_graphics_items def mousePressEvent(self, event): super().mousePressEvent(event) - def load_layout_from_file(self, file_path: str) -> None: + @Slot() + def load_layout(self) -> None: """ - Loads a layout for a processor from a saved filed. + Prompt the user for a layout file and load the layout from the file. """ - file = open(file_path) - - graphics_item_name = None - graphics_item_str = "" - # Go through each line in the file - for line in file.readlines(): - # If no name currently saved then get name from current line - if graphics_item_name is None: - graphics_item_name = line.partition(":")[0] + # Prompt the user for a file + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + dialog.setDirectory("~/simudator") # TODO: does this work when exported? + path = dialog.getOpenFileName()[0] - # if already has name get line content - else: - # if not empty line, then save content - if line.strip(): - graphics_item_str += line + # If no file was selected, do nothing + if path == '': + return - # if line is empty then give saved content to name file - else: - graphics_item = self.get_module(graphics_item_name) - try: - graphics_item.load_state_from_str(graphics_item_str) - except Exception as exc: - raise exc - - # set back to empty - graphics_item_str = "" - graphics_item_name = None + try: + self.load_layout_from_file(path) - # give module anything that is left - if graphics_item_name and graphics_item_str: - graphics_item = self.get_module(graphics_item_name) - graphics_item.load_state_from_str(graphics_item_str) + # Anything goes wrong with loading the selected one, + # we dont care about what went wrong + except (OSError, JSONDecodeError): + self.load_default_layout() + self._error_msg_box.showMessage( + "Unable to load given file.", "load_layout_err" + ) + else: + QMessageBox.information( + self.parent(), "SimuDator", "File loaded succesfully." + ) - def save_layout_to_file(self, file_path: str) -> None: + def _save_layout_to_file(self, file_path: str) -> None: """ - Saves the positions and visibility of graphicsitems in the scene (this - includes module and signal representations) to a file. + Save the layout of the scene to file. + + Parameters + ---------- + file_path : str + Path to the file to save to layout to. """ layout_str = "" - for graphics_item in self.module_graphics_items.values(): + for graphics_item in self._module_graphics_items.values(): layout_str += graphics_item.save_state_as_str() + "\n" file = open(file_path, "w") file.write(layout_str) file.close() - def setAllSignalsVisibility(self, is_signals_visible: bool) -> None: - for item in self.signal_graphics_items: - item.setVisible(is_signals_visible) + @Slot(bool) + def show_all_signals(self, value: bool) -> None: + """ + Set the visibility of all graphical signals in the scene. - def setPortNamesVisibility(self, is_ports_visible: bool) -> None: - for item in self.module_graphics_items.values(): + Parameters + ---------- + value : bool + ``True`` to show all graphical signals, ``False`` to hide them. + """ + for item in self._signal_graphics_items: + item.setVisible(value) + + @Slot(bool) + def show_port_names(self, value: bool) -> None: + """ + Set the visibility of the names of all ports of graphical modules + in the scene. + + Parameters + ---------- + value : bool + ``True`` to show the port names, ``False`` to hide them. + """ + for item in self._module_graphics_items.values(): for port in item.ports: - port.setNameVisibility(is_ports_visible) + port.setNameVisibility(value) - def setLayoutLock(self, is_layout_locked: bool) -> None: + def toggle_layout_lock(self, value: bool) -> None: """ - Toggles the layout lock making it so items in the scene can not be moved. + Toggle the layout between locked and unlocked. Locked means that + nothing can be moved around in the layout. + + Parameters + ---------- + value : bool + ``True`` to lock the layout, ``False`` to unlock it. """ - for item in self.module_graphics_items.values(): - item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked) - item.setLocked(is_layout_locked) + for item in self._module_graphics_items.values(): + item.setFlag(QGraphicsItem.ItemIsMovable, not value) + item.setLocked(value) - for item in self.signal_graphics_items: - item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked) + for item in self._signal_graphics_items: + item.setFlag(QGraphicsItem.ItemIsMovable, not value) # We use this value so lines in the signal can not be moved or edited - item.is_locked = is_layout_locked + item.is_locked = value + + @Slot() + def save_layout(self) -> None: + """ + Prompt the user for a file and save the scene layout to the file. + + This overwrites the previous content of the file. + """ + # Prompt the user for a file + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + path = dialog.getSaveFileName()[0] + + # Open the given file erasing the previous content + with open(path, "w") as fp: + graphics_modules_data = {} + ports_data = {} + graphics_signals_data = {} + + graphics_modules = self.get_modules() + for graphics_module in graphics_modules: + pos = (graphics_module.x(), graphics_module.y()) + graphics_modules_data[graphics_module.getName()] = pos + + for port in graphics_module.getPorts(): + visibility = port.isVisible() + orientation = int(port.getOrientation()) + data = (port.x(), port.y(), orientation, visibility) + ports_data[port.getID()] = data + + for graphics_signal in self.get_signals(): + visibility = graphics_signal.isVisible() + points = [] + for point in graphics_signal.getPoints(): + points.append((point.x(), point.y())) + data = (points, visibility) + graphics_signals_data[graphics_signal.getID()] = data + + data = (graphics_modules_data, ports_data, graphics_signals_data) + json.dump(data, fp) + + fp.close() + + @Slot() + def load_default_layout(self) -> None: + """ + Place all graphical modules in the scene according to the default + layout, i.e. along a diagonal. + """ + counter = 0 + for key in self._module_graphics_items: + module_graphic = self._module_graphics_items[key] + module_graphic.showPorts() + counter += 1 + self.move_module_to(module_graphic, counter, counter) + self.reset_signals() + + def load_layout_from_file(self, file_path: str) -> None: + """ + Load a layout from a file. + + If at anypoint this function would error, the default layout + is loaded instead. + + Parameters + ---------- + file_path : str + Path to a file containing a layout for the processor scene. + """ + graphics_modules = self._module_graphics_items + ports = {} + graphics_signals = {} + + for graphics_module in graphics_modules.values(): + for port in graphics_module.getPorts(): + ports[port.getID()] = port + + for graphics_signal in self.get_signals(): + graphics_signals[graphics_signal.getID()] = graphics_signal + + # Open the file in 'read-only' + with open(file_path, 'rb') as fp: + data = json.load(fp) + graphics_modules_data = data[0] + ports_data = data[1] + graphics_signals_data = data[2] + + for g_module_name, g_module_data in graphics_modules_data.items(): + g_module = graphics_modules[g_module_name] + x = g_module_data[0] + y = g_module_data[1] + self._load_module(g_module, x, y) + + for port_id, port_data in ports_data.items(): + port = ports[int(port_id)] + self._load_port(port, *port_data) + + for g_signal_id, g_signal_data in graphics_signals_data.items(): + g_signal = graphics_signals[int(g_signal_id)] + self._load_signal(g_signal, *g_signal_data) + + fp.close() + + def _load_signal( + self, + signal: SignalGraphicsItem, + signal_points: list[tuple[float, float]], + visibility: bool, + ) -> None: + """ + Set the positions and visibility of a graphical signal. Helper method + for loading a layout from file. + + Parameters + ---------- + signal : SignalGraphicsItem + Graphical signal to modify. + signal_points : list[tuple[float, float]] + List of points for visually drawing the signal as line. + visibility : bool + Visibility of the signal. ``True`` to show it, ``False`` to hide it. + """ + qpoints = [] + # Turn points -> QPointF + # list[int, int] -> QPointF object + for point in signal_points: + qpoints.append(QPointF(point[0], point[1])) + + # Set the new points + signal.setPoints(qpoints) + signal.setVisible(visibility) + + def _load_module( + self, + module: ModuleGraphicsItem, + pos_x: float, + pos_y: float, + ) -> None: + """ + Set the position of a graphical module in the scene. Helper method + for loading a layout from file. + + Parameters + ---------- + module : ModuleGraphicsItem + Graphical module of which to set the position. + pos_x : float + Position on the x-axis in the scene. + pos_y : float + Position on the y-axis in the scene. + """ + module.setX(pos_x) + module.setY(pos_y) + + def _load_port( + self, + port: PortGraphicsItem, + pos_x: float, + pos_y: float, + orientation: Orientation, + visibility: bool, + ) -> None: + """ + Set position, orientation and visibility of a port of a graphical + module. Helper method for loading a layout from file. + + Parameters + ---------- + port : PortGraphicsItem + Port to modify. + pos_x : float + Position on the x-axis in the scene. + pos_y : float + Position on the y-axis in the scene. + orientation : Orientation + Orientation of the port. + visibility : bool + Visibility of the port. ``True`` to show the port, ``False`` to + hide it. + """ + port.setOrientation(orientation) + port.setX(pos_x) + port.setY(pos_y) + port.setVisible(visibility) diff --git a/src/simudator/gui/custom_toolbar.py b/src/simudator/gui/custom_toolbar.py deleted file mode 100644 index b55049f2cb0848b4ca1b8b294e734d06fd9a429b..0000000000000000000000000000000000000000 --- a/src/simudator/gui/custom_toolbar.py +++ /dev/null @@ -1,14 +0,0 @@ -from qtpy.QtWidgets import QToolBar - - -class CustomToolBar(QToolBar): - """ - A custom implementation of QToolBar that reimplemented contextMenuEvent - so that the toolbar is no longer removable. - """ - - def __init__(self, text): - super().__init__(text) - - def contextMenuEvent(self, event): - pass diff --git a/src/simudator/gui/gui.py b/src/simudator/gui/gui.py index 48297b3901f484398cbe607b8ae512c22b96b188..2f2aa64626b65082ff9a321e61de017495bfa6fc 100644 --- a/src/simudator/gui/gui.py +++ b/src/simudator/gui/gui.py @@ -1,137 +1,63 @@ import ast -import json import sys -from json import JSONDecodeError -from threading import Thread from qtpy import QtCore, QtWidgets -from qtpy.QtCore import QPointF from qtpy.QtCore import Signal as pyqtSignal -from qtpy.QtCore import Slot +from qtpy.QtCore import Slot, ws from qtpy.QtWidgets import ( QAction, QApplication, QErrorMessage, - QFileDialog, - QGraphicsView, - QInputDialog, - QLabel, QMainWindow, QMessageBox, - QSpinBox, - QStyle, - QWidget, ) from simudator.core.processor import Processor from simudator.gui.breakpoint_window import BreakpointWindow from simudator.gui.cpu_graphics_scene import CpuGraphicsScene -from simudator.gui.custom_toolbar import CustomToolBar from simudator.gui.dialogs.lambda_breakpoint_dialog import LambdaBreakpointDialog +from simudator.gui.menu_bar import MainMenuBar from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem -from simudator.gui.orientation import Orientation -from simudator.gui.port_graphics_item import PortGraphicsItem -from simudator.gui.run_continuously_thread import RunThread -from simudator.gui.signal_graphics_item import SignalGraphicsItem from simudator.gui.pipeline import PipeLine - - -class View(QGraphicsView): - """ - This class views all QGraphicsItems for the user. It takes the - QGraphicsScene as input and inherits all funcitonality from the - QGraphicsView and overrides the wheelEvent function. - This allows the users to navigate the view with their trackpads - and zoom in/out with their trackpads + ctrl. - """ - - def __init__(self, QGraphicsScene): - super().__init__(QGraphicsScene) - self.scene = QGraphicsScene - - def wheelEvent(self, event): - """ - Default behaviour if ctrl is not pressed, otherwise zoom in/out. - """ - modifiers = QtWidgets.QApplication.keyboardModifiers() - if modifiers == QtCore.Qt.ControlModifier: - # Factor above 1 zooms in, below zooms out - factor = 1.03 - if event.angleDelta().y() < 0: - factor = 0.97 - - # If event got triggered due to the x axis, do nothing - if event.pixelDelta().x() != 0: - return - - view_pos = event.globalPosition() - scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y())) - - self.centerOn(scene_pos) - self.scale(factor, factor) - - old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y())) - new_mapToScene = self.mapToScene(self.viewport().rect().center()) - - delta = old_mapToScene - new_mapToScene - - self.centerOn(scene_pos - delta) - - else: - # Default behaviour - super().wheelEvent(event) +from simudator.gui.processor_handler import ProcessorHandler +from simudator.gui.simulation_toolbar import SimulationToolBar +from simudator.gui.view import View class GUI(QMainWindow): """ - Main gui class. Handles gui windows, toolbar and visualizes modules. + Main gui class for visualizing the SimuDator processor simulator. - This is the main class for the GUI. It handles creating the window for - the gui, aswell as the toolbar for controlling the simultaion. - It takes a processor and visualizes its modules as boxes with the signals as lines - between them. Graphics items for the modules and signals need to be created - and added individually. - """ + Graphics items for modules need to be created and added individually using + the provided methods. Signals are also not added automatically but need + not be created. - cpu_tick_signal = pyqtSignal(int) - HALT_MESSAGE_THRESHOLD = 100 + Parameters + ---------- + processor : Processor + The simulated processor to visualize and control. + """ - def __init__(self, cpu: Processor): + def __init__(self, processor: Processor): super().__init__() - self.cpu = cpu + self._processor = processor + self._processor_handler = ProcessorHandler(processor, parent=self) self.setWindowTitle("SimuDator") - self.cpu_graphics_scene = CpuGraphicsScene(cpu) - self.graphics_view = View(self.cpu_graphics_scene) + self._graphics_scene = CpuGraphicsScene() + self._graphics_view = View(self._graphics_scene) # self.graphics_view.setDragMode(True) - self.moduleActions: dict[str, QAction] = {} - - self.setCentralWidget(self.graphics_view) - - self.errorMessageWidget = QErrorMessage(self) - - self.createToolBar() + self._module_actions: dict[str, QAction] = {} - # Threadpool for running cpu and gui concurently - self.threadpool = QtCore.QThreadPool() + self.setCentralWidget(self._graphics_view) - # Signal to tell gui when cpu has halted - self.cpu_tick_signal.connect(self.handleCpuTick) + self._error_msg_widget = QErrorMessage(self) - # Used to set if all values in the gui should be updated each tick - # or only the clock counter - self.update_all_values = False + self._create_menu_bar() + self._create_tool_bar() - # Used to set the update delay - # Useful when watching the values being updated while running - self.update_delay = 0.00 - - # Used to lock some actions in ui when cpu is running in another thread - # Using the cpu's internal status directly could case problems - self.cpu_running = False - - self.pipeline = PipeLine() + self._pipeline = PipeLine() self.init_pipeline() # Set Style, THESE ARE TEST AND DONT WORK @@ -140,196 +66,58 @@ class GUI(QMainWindow): self.setAttribute(QtCore.Qt.WA_StyledBackground) # Add this so we can save the window in scope later, otherwise it disappears - self.breakpoint_window = None + self._breakpoint_window = None - def createToolBar(self) -> None: + def _create_menu_bar(self) -> None: """ - Creates the toolbar containing file, layout and toolbar buttons. - - - Creates the toolbar containing the file, layout and toolbar - buttons with their respective sub buttons. + Create a main menu bar and connect its signals to appropriate slots of + other widgets. """ - # Create toolbar - toolbar = CustomToolBar("Main toolbar") - self.addToolBar(toolbar) - - # Create load action - self.load_action = QAction("Load", self) - self.load_action.setStatusTip("Load processor state from file") - self.load_action.triggered.connect(self.loadToolBarButtonClick) - - # Create save action - self.save_action = QAction("Save", self) - self.save_action.setStatusTip("Save processor state to file") - self.save_action.triggered.connect(self.saveStateMenuButtonClick) - - self.reset_action = QAction("Reset", self) - self.reset_action.setStatusTip("Reset processor") - self.reset_action.triggered.connect(self.resetMenuButtonClick) - - # Create File menu for load and save - menu = self.menuBar() - file_menu = menu.addMenu("&File") - file_menu.addAction(self.load_action) - file_menu.addAction(self.save_action) - file_menu.addAction(self.reset_action) - - # create load layout action - load_layout_action = QAction("Load layout", self) - load_layout_action.setStatusTip("Loads the selected layout from file.") - load_layout_action.triggered.connect(self.loadLayoutToolBarButtonClick) - - # create load default layout action - load_default_layout_action = QAction("Load default layout", self) - load_default_layout_action.setStatusTip("Loads the default layout from file.") - load_default_layout_action.triggered.connect( - self.loadDefaultLayoutToolBarButtonClick + self._menu_bar = MainMenuBar(self) + self.setMenuBar(self._menu_bar) + + # Connect signals for processor related actions + self._menu_bar.load.connect(self._processor_handler.load_state) + self._menu_bar.save.connect(self._processor_handler.save_state) + self._menu_bar.reset.connect(self._processor_handler.reset) + self._menu_bar.update_all_values.connect( + self._processor_handler.toggle_value_update_on_run ) + self._menu_bar.set_delay.connect(self._processor_handler.set_update_delay) - # create save layout action - save_layout_action = QAction("Save layout", self) - save_layout_action.setStatusTip("Saves the current layout to a file.") - save_layout_action.triggered.connect(self.saveLayoutToolBarButtonClick) - - # create layout lock action - self.lock_layout_action = QAction("Lock layout", self, checkable=True) - self.lock_layout_action.setStatusTip("Lock layout so items can't be moved.") - self.lock_layout_action.triggered.connect(self.toggleLayoutLockMenuButtonClick) - - # Create show signals actions - self.signal_vis_action = QAction("Show signals", self, checkable=True) - self.signal_vis_action.setChecked(True) - self.signal_vis_action.setStatusTip("Toggle the visibility of signal.") - self.signal_vis_action.triggered.connect(self.showSignalsMenuButtonClick) - - # Create show port name actions - self.port_vis_action = QAction("Show port names", self, checkable=True) - self.port_vis_action.setChecked(True) - self.port_vis_action.setStatusTip("Toggle the visibility of port names.") - self.port_vis_action.triggered.connect(self.showPortNamesBarButtonClick) - - # Create Layout menu for layout actions - layout_menu = menu.addMenu("&Layout") - layout_menu.addAction(load_layout_action) - layout_menu.addAction(load_default_layout_action) - layout_menu.addAction(save_layout_action) - layout_menu.addAction(self.lock_layout_action) - layout_menu.addAction(self.signal_vis_action) - layout_menu.addAction(self.port_vis_action) - - # Create breakpoint window action - self.breakpoint_action = QAction("Breakpoints", self) - self.breakpoint_action.setStatusTip("Open breakpoint window.") - self.breakpoint_action.triggered.connect(self.openBreakpointWindow) - - # Create 'update value' window button - self.update_value_action = QAction( - "Update values while running", self, checkable=True + # Connect signals for managing the processor layout + self._menu_bar.load_layout.connect(self._graphics_scene.load_layout) + self._menu_bar.load_default_layout.connect( + self._graphics_scene.load_default_layout ) - self.update_value_action.setChecked(False) - self.update_value_action.setStatusTip("Toggle value updates while running.") - self.update_value_action.triggered.connect(self.toggle_value_update_on_run) - - # Create 'set delay' window button - self.set_delay_action = QAction("Set update delay", self) - self.set_delay_action.setStatusTip( - "Sets the delay between each update when the cpu is running." + self._menu_bar.save_layout.connect(self._graphics_scene.save_layout) + self._menu_bar.lock_layout.connect(self._graphics_scene.toggle_layout_lock) + self._menu_bar.show_all_signals.connect(self._graphics_scene.show_all_signals) + self._menu_bar.show_port_names.connect(self._graphics_scene.show_port_names) + + # Connect signal for managing breakpoints + self._menu_bar.show_breakpoints.connect(self.openBreakpointWindow) + + self._processor_handler.running.connect( + self._menu_bar.set_disabled_when_running ) - self.set_delay_action.triggered.connect(self.set_update_delay) - - # Create Tools menu for tool actions actions - tools_menu = menu.addMenu("&Tools") - tools_menu.addAction(self.breakpoint_action) - tools_menu.addAction(self.update_value_action) - tools_menu.addAction(self.set_delay_action) - - # Add run button on toolbar - arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay) - self.run_action = QAction(arrow_icon, "Run", self) - self.run_action.setStatusTip("Run until halt") - self.run_action.triggered.connect(self.runToolBarButtonClick) - toolbar.addAction(self.run_action) - - # Add stop button on toolbar - stop_icon = self.style().standardIcon(QStyle.SP_MediaStop) - self.stop_action = QAction(stop_icon, "Stop", self) - self.stop_action.setStatusTip("Stop running") - self.stop_action.triggered.connect(self.stopToolBarButtonClick) - toolbar.addAction(self.stop_action) - - # Add Asm label - self.clock_cycle_label = QLabel("Clock cycle: ", self) - toolbar.addWidget(self.clock_cycle_label) - - # Add undo button on toolbar - backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) - self.undo_action = QAction(backward_arrow_icon, "Undo", self) - self.undo_action.setStatusTip("Undo the last processor tick") - self.undo_action.triggered.connect(self.undoToolBarButtonClick) - toolbar.addAction(self.undo_action) - - # Add step button on toolbar - forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) - self.step_action = QAction(forward_arrow_icon, "Step", self) - self.step_action.setStatusTip("Run one clock cycle") - self.step_action.triggered.connect(self.stepToolBarButtonClick) - toolbar.addAction(self.step_action) - - # Add box for jump value - self.jump_value_box = QSpinBox() - self.jump_value_box.setMinimum(999999) - self.jump_value_box.setMinimum(1) - self.jump_value_box.setValue(1) - toolbar.addWidget(self.jump_value_box) - - # Add seperator so clock gets better spacing - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - toolbar.addSeparator() - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - - # Add clock counter - self.clock_label = QLabel("Clock cycles: " + str(self.cpu.get_clock()), self) - toolbar.addWidget(self.clock_label) - - # Add seperator so clock gets better spacing - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - toolbar.addSeparator() - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - - # Add Asm label - self.asm_label = QLabel("Assembler Instructions: ", self) - toolbar.addWidget(self.asm_label) - - # Add undo asm button on toolbar - backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) - self.undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self) - self.undo_asm_action.setStatusTip("Undo the last assembler instruction") - self.undo_asm_action.triggered.connect(self.undoAsmToolBarButtonClick) - toolbar.addAction(self.undo_asm_action) - - # Add step button on toolbar - forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) - self.step_asm_action = QAction(forward_arrow_icon, "Step Asm", self) - self.step_asm_action.setStatusTip("Run one assembler instruction") - self.step_asm_action.triggered.connect(self.stepAsmToolBarButtonClick) - toolbar.addAction(self.step_asm_action) - - # Add box for jump value - self.asm_jump_value_box = QSpinBox() - self.asm_jump_value_box.setMinimum(999999) - self.asm_jump_value_box.setMinimum(1) - self.asm_jump_value_box.setValue(1) - toolbar.addWidget(self.asm_jump_value_box) + + def _create_tool_bar(self) -> None: + """ + Create a simulation toolbar and connect its signals to appropriate + slots of other widgets. + """ + self._toolbar = SimulationToolBar("Main toolbar", self) + self.addToolBar(self._toolbar) + self._toolbar.run.connect(self._processor_handler.run_simulation) + self._toolbar.stop.connect(self._processor_handler.stop_simulation) + self._toolbar.step.connect(self._processor_handler.step_cycles) + self._toolbar.undo.connect(self._processor_handler.undo_cycles) + self._toolbar.step_asm.connect(self._processor_handler.step_asm_instructions) + self._toolbar.undo_asm.connect(self._processor_handler.undo_asm_instructions) + self._processor_handler.cycle_changed.connect(self._toolbar.update_clock) + self._processor_handler.running.connect(self._toolbar.set_disabled_when_running) def connectModuleActions(self, action_signals: []) -> None: """ @@ -357,50 +145,30 @@ class GUI(QMainWindow): # LambdaBPs are not tested since we dont # actually have any modules that make them yet signal.connect(self.addLambdaBreakpoint) - case "UPDATE": - signal.connect(self.updateCpuListeners) - case "CLOCKUPDATE": - signal.connect(self.updateCpuClockCycle) def init_pipeline(self) -> None: - """Initialize the pipeline diagram. + """Initialize the pipeline diagram. Sets its height, width and instructions. """ - size = self.cpu.get_pipeline_dimensions() - - self.pipeline.set_height(size[0]) - self.pipeline.set_width(size[1]) - self.update_pipeline() + size = self._processor.get_pipeline_dimensions() - self.pipeline.show() + self._pipeline.set_height(size[0]) + self._pipeline.set_width(size[1]) - def updateCpuListeners(self) -> None: - """ - Updates the graphics items in the scene, the clock, and the pipeline diagram. - - Used after the cpu has run or when the user has edited somehting. - """ - self.cpu_graphics_scene.updateGraphicsItems() - self.updateCpuClockCycle() - self.update_pipeline() - - def update_pipeline(self) -> None: - self.pipeline.set_instructions(self.cpu.get_current_instructions()) - - def updateCpuClockCycle(self) -> None: - """ - Update the clock cycle counter. - - Used while the program is running to show the user nothing has crashed. - """ - self.clock_label.setText("Clockcycle: " + str(self.cpu.get_clock())) + self._processor_handler.changed_instruction.connect( + self._pipeline.set_instructions + ) + # TODO: Find prettier way of making the processor handler emit its + # signal for the current processor instructions + self._processor_handler._signal_processor_changed() + self._pipeline.show() def lambdaBreakpointDialog(self) -> None: """ Opens dialog window for user to create a breakpoint. """ - lambdas = self.cpu.get_breakpoint_lambdas() + lambdas = self._processor.get_breakpoint_lambdas() lambda_br_dialog = LambdaBreakpointDialog(lambdas, self) lambda_br_dialog.accepted.connect(self.addLambdaBreakpoint) @@ -409,8 +177,8 @@ class GUI(QMainWindow): Updates the breakpoint window when new breakpoints are added. """ # Don't do anything if window is closed - if self.breakpoint_window is not None: - self.breakpoint_window.update() + if self._breakpoint_window is not None: + self._breakpoint_window.update() """ @Slot is used to explicitly mark a python method as a Qt slot @@ -428,13 +196,13 @@ class GUI(QMainWindow): try: parsed_value = ast.literal_eval(value) except SyntaxError as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) else: - module = self.cpu.get_module(module_name) + module = self._processor.get_module(module_name) module_state = module.get_state() module_state[state] = parsed_value module.set_state(module_state) - self.cpu_graphics_scene.updateGraphicsItems() + self._graphics_scene.update_modules() @Slot(str, str, str) def editMemoryContent(self, module_name: str, adress: str, value: str) -> None: @@ -447,17 +215,17 @@ class GUI(QMainWindow): parsed_adress = int(adress, 16) parsed_value = ast.literal_eval(value) except SyntaxError as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) except ValueError: - self.errorMessageWidget.showMessage( + self._error_msg_widget.showMessage( "You must enter a hexadecimal" "number preceeded by '0x' (e.g." "0xc3)." ) else: - module = self.cpu.get_module(module_name) + module = self._processor.get_module(module_name) module_state = module.get_state() module_state['memory'][parsed_adress] = parsed_value module.set_state(module_state) - self.cpu_graphics_scene.updateGraphicsItems() + self._graphics_scene.update_modules() @Slot(str, str, str) def addStateBreakpoint(self, module_name: str, state: str, value: str) -> None: @@ -466,10 +234,10 @@ class GUI(QMainWindow): """ try: parsed_value = ast.literal_eval(value) - self.cpu.add_state_breakpoint(module_name, state, parsed_value) + self._processor.add_state_breakpoint(module_name, state, parsed_value) self.updateBreakpointWindow() except (ValueError, SyntaxError) as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) @Slot(str, str) def addLambdaBreakpoint(self, lambda_name, kwargs_str) -> None: @@ -481,10 +249,10 @@ class GUI(QMainWindow): for kwarg in kwargs_str.split(' '): key, value = kwarg.split('=') lambda_kwargs[key] = ast.literal_eval(value) - self.cpu.add_lambda_breakpoint(lambda_name, **lambda_kwargs) + self._processor.add_lambda_breakpoint(lambda_name, **lambda_kwargs) self.updateBreakpointWindow() except (ValueError, SyntaxError) as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) @Slot(str, str, str) def addMemoryBreakpoint(self, module_name: str, adress: str, value: str) -> None: @@ -494,498 +262,56 @@ class GUI(QMainWindow): try: parsed_adress = int(adress) parsed_value = int(value) - self.cpu.add_memory_breakpoint(module_name, parsed_adress, parsed_value) + self._processor.add_memory_breakpoint( + module_name, parsed_adress, parsed_value + ) self.updateBreakpointWindow() except SyntaxError as e: - self.errorMessageWidget.showMessage(str(e)) - - def setDisabledWhenRunning(self, is_disable): - """ - Greys out buttons for actions that can't be done while cpu is running. - """ - self.load_action.setDisabled(is_disable) - self.save_action.setDisabled(is_disable) - self.reset_action.setDisabled(is_disable) - self.run_action.setDisabled(is_disable) - self.step_action.setDisabled(is_disable) - self.undo_action.setDisabled(is_disable) - - def stepToolBarButtonClick(self): - """ - Runs the cpu a specified number of clock cycles according to the jump value box. - """ - - # Don't do steps if cpu is running - if self.cpu_running: - return - - steps = self.jump_value_box.value() - self.cpu_running = True - self.setDisabledWhenRunning(True) - simulation_thread = RunThread( - self.cpu, self.cpu_tick_signal, self.update_delay, False, False, steps - ) - self.threadpool.start(simulation_thread) - - def stepAsmToolBarButtonClick(self): - """ - Runs the cpu a specified number of asm instructions according to the jump value box. - """ - - # Don't do steps if cpu is running - if self.cpu_running: - return - - steps = self.asm_jump_value_box.value() - self.cpu_running = True - self.setDisabledWhenRunning(True) - simultaion_thread = RunThread( - self.cpu, self.cpu_tick_signal, self.update_delay, False, True, steps - ) - self.threadpool.start(simultaion_thread) - self.updateCpuListeners() - - def runToolBarButtonClick(self) -> None: - """ - Runs the cpu until it is stopped by user input, breakpoint or similar. - """ - - # Don't run if already running - if self.cpu_running: - return - - # Create own thread for cpu simulation so gui dosent freeze - self.cpu_running = True - self.setDisabledWhenRunning(True) - simulation_thread = RunThread(self.cpu, self.cpu_tick_signal, self.update_delay) - self.threadpool.start(simulation_thread) - - @Slot(int) - def handleCpuTick(self, steps: int) -> None: - """ - Called from other thread after every cpu tick. - Will inform the user and update visuals. - """ - - # Update cpu clock counter every tick - self.updateCpuClockCycle() - - if self.update_all_values: - self.updateCpuListeners() - - # A signal of 0 steps signifies end of execution, i.e. the CPU has - # halted or run the specified amount of ticks - # => Enable the relevant parts of the GUI again - if steps == 0: - self.cpu_running = False - self.setDisabledWhenRunning(False) - self.updateCpuListeners() - - # Inform user of reached break point - if self.cpu.breakpoint_reached: - self.messageBox( - "Reached breakpoint: " + self.cpu.last_breakpoint.__str__() - ) - - # Inform user of halt - if self.cpu.should_halt(): - self.messageBox("The processor halted.") - - def stopToolBarButtonClick(self) -> None: - """ - Tells the cpu to stop. It will then stop at an appropriate in its own thread. - """ - self.cpu.stop() - - def folderSaveDialog(self) -> str: - """ - Open a file explorer in a new window. Return the absolute path to the selected file. - - Can return existing as well as non-existing files. - If the selected files does not exist it will be created. - """ - dialog = QFileDialog() - dialog.setFileMode(QFileDialog.AnyFile) - dialog.setAcceptMode(QFileDialog.AcceptOpen) - dialog.setDirectory("~/simudator") # TODO: does this work when exported? - - # getSaveFileName() can returncan return existing file but also create new ones - return dialog.getSaveFileName()[0] - - def folderLoadDialog(self) -> str: - """ - Open a file explorer in a new window. Return the absolute path to the selected file. - - Can only return existing files. - """ - dialog = QFileDialog() - dialog.setFileMode(QFileDialog.AnyFile) - dialog.setAcceptMode(QFileDialog.AcceptOpen) - dialog.setDirectory("~/simudator") # TODO: does this work when exported? - - # getOpenFileName() will only return already existing files - return dialog.getOpenFileName()[0] - - def loadToolBarButtonClick(self) -> None: - """ - Loads the processor state from selected file. - """ - - # not safe to load while cpu is running - if self.cpu_running: - return - - path = self.folderLoadDialog() - try: - self.cpu.load_state_from_file(path) - except KeyError: - self.errorBox("Could not load selected file.") - except ValueError: - self.errorBox("Selected file was empty.") - except FileNotFoundError: - # This error is triggered when no file was selected by - # by the user. The user should know they did not select a - # file and therefore there no need to display a box telling - # them that. - pass - # self.errorBox("No vaild file was selected.") - else: - self.messageBox("Loaded file successfully.") - self.updateCpuListeners() - - def resetMenuButtonClick(self) -> None: - """ - Will reset processor to inital values. - """ - - # not safe to reset while cpu running - if self.cpu_running: - return - - answer = QMessageBox.question( - self, "Reset Processor", "Are you sure you want to reset the processor?" - ) - - if answer == QMessageBox.Yes: - self.cpu.reset() - self.updateCpuListeners() - - def undoNToolBarButtonClick(self) -> None: - # TODO: I dont think this is used - """ - Undo zero to N cycles. - """ - - # Don't try to undo while running - if self.cpu_running: - return - - cycles, ok = QInputDialog(self).getInt( - self, - "Input number of cycles to run", - "Input number of cycles to run", - ) - if ok: - if cycles < 1: - self.errorBox("Please input a number larger than 0.") - return - try: - # Make sure we load 0 as lowest - self.cpu.load_cycle(max(self.cpu.get_clock() - cycles, 0)) - self.updateCpuListeners() - except (ValueError, IndexError): - self.errorBox("Unable to undo the cycle(s).") - - def saveLayoutToolBarButtonClick(self) -> None: - """ - Saves the layout of all the modules in a (somewhat) human readable format. - - This also erases the previous content of the file before saving - the data. - """ - path = self.folderSaveDialog() - - # Change file to the selected file if a file was selected - if path == '': - return - - # Open the given file erasing the previous content - with open(path, "w") as fp: - graphics_modules_data = {} - ports_data = {} - graphics_signals_data = {} - - graphics_modules = self.cpu_graphics_scene.getModulesGraphicsItems() - for graphics_module in graphics_modules: - pos = (graphics_module.x(), graphics_module.y()) - graphics_modules_data[graphics_module.getName()] = pos - - for port in graphics_module.getPorts(): - visibility = port.isVisible() - orientation = int(port.getOrientation()) - data = (port.x(), port.y(), orientation, visibility) - ports_data[port.getID()] = data - - for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems(): - visibility = graphics_signal.isVisible() - points = [] - for point in graphics_signal.getPoints(): - points.append((point.x(), point.y())) - data = (points, visibility) - graphics_signals_data[graphics_signal.getID()] = data - - data = (graphics_modules_data, ports_data, graphics_signals_data) - json.dump(data, fp) - - fp.close() - - def loadDefaultLayoutToolBarButtonClick(self) -> None: - """ - Places all the module_graphic objects in a diagonal going down to the right. - """ - counter = 0 - for key in self.cpu_graphics_scene.module_graphics_items: - module_graphic = self.cpu_graphics_scene.module_graphics_items[key] - module_graphic.showPorts() - counter += 1 - self.cpu_graphics_scene.replaceModuleGraphicsItem( - module_graphic, counter, counter - ) - self.cpu_graphics_scene.resetSignals() - - def loadLayoutFromFile(self, file) -> None: - """ - Loads a layout for the current cpu from the selected file. - - If at anypoint this funciton would error, the default layout - is loaded instead. - """ - # TODO: speed(?) - graphics_modules = self.cpu_graphics_scene.moduleGraphicsItemsDict() - ports = {} - graphics_signals = {} - - for graphics_module in graphics_modules.values(): - for port in graphics_module.getPorts(): - ports[port.getID()] = port - - for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems(): - graphics_signals[graphics_signal.getID()] = graphics_signal - - # Open the file in 'read-only' - with open(file, 'rb') as fp: - data = json.load(fp) - graphics_modules_data = data[0] - ports_data = data[1] - graphics_signals_data = data[2] - - for g_module_name, g_module_data in graphics_modules_data.items(): - g_module = graphics_modules[g_module_name] - x = g_module_data[0] - y = g_module_data[1] - self.loadGraphicsModule(g_module, x, y) - - for port_id, port_data in ports_data.items(): - port = ports[int(port_id)] - self.loadPort(port, *port_data) - - for g_signal_id, g_signal_data in graphics_signals_data.items(): - g_signal = graphics_signals[int(g_signal_id)] - self.loadSignal(g_signal, *g_signal_data) - - fp.close() - - def loadSignal( - self, - graphic_signal: SignalGraphicsItem, - signal_points: list[tuple[float, float]], - visibility: bool, - ) -> None: - """ - Changes the graphical signal to have the positions given as argument. - """ - qpoints = [] - # Turn points -> QPointF - # list[int, int] -> QPointF object - for point in signal_points: - qpoints.append(QPointF(point[0], point[1])) - - # Set the new points - graphic_signal.setPoints(qpoints) - graphic_signal.setVisible(visibility) - - def loadGraphicsModule( - self, - graphics_module: ModuleGraphicsItem, - graphics_module_x: float, - graphics_module_y: float, - ) -> None: - """ - Changes the positions of graphical modules to the ones given as argument. - """ - graphics_module.setX(graphics_module_x) - graphics_module.setY(graphics_module_y) - - def loadPort( - self, - port: PortGraphicsItem, - x: float, - y: float, - orientation: Orientation, - visibility: bool, - ) -> None: - port.setOrientation(orientation) - port.setX(x) - port.setY(y) - port.setVisible(visibility) - - def saveStateMenuButtonClick(self) -> None: - """ - Save state of cpu to file. - - This erases the previous content of the file before saving the data. - """ - - # Not safe to save while running - if self.cpu_running: - return - - path = self.folderSaveDialog() - - # Change file to the selected file if a file was selected - if path == '': - return - - res = self.cpu.save_state_to_file(path) - - if res: - self.messageBox("File saved.") - else: - self.errorBox("Unable to save.") + self._error_msg_widget.showMessage(str(e)) + @Slot() def openBreakpointWindow(self) -> None: """ Opens window for editing breakpoints. """ - self.breakpoint_window = BreakpointWindow(self.cpu) - self.breakpoint_window.show() + self._breakpoint_window = BreakpointWindow(self._processor) + self._breakpoint_window.show() - def toggle_value_update_on_run(self): - """ - Toggles whether all values or only clock cycle is being updated each tick. - """ - self.update_all_values = not self.update_all_values - - def set_update_delay(self): - """ - Sets the update delay for the visual updates while the cpu is running. - """ - delay, ok = QInputDialog.getDouble( - self, "Input Dialog", "Enter a float value:", decimals=5 - ) - if ok: - self.update_delay = delay - - def showPortNamesBarButtonClick(self): - """ - Toggles showing port names in the graphics scene. - """ - self.cpu_graphics_scene.setPortNamesVisibility(self.port_vis_action.isChecked()) - - def showSignalsMenuButtonClick(self) -> None: - """ - Toggle shoing the signals in the graphics scene. - """ - self.cpu_graphics_scene.setAllSignalsVisibility( - self.signal_vis_action.isChecked() - ) - - def toggleLayoutLockMenuButtonClick(self) -> None: - """ - Toggles so the layout can not be edited. - """ - self.cpu_graphics_scene.setLayoutLock(self.lock_layout_action.isChecked()) - - def loadLayoutToolBarButtonClick(self) -> None: - """ - Loads a given layout from a selected file. - - If the layout was - unable to load, an error message will pop up informing the user - and the default layout will be loaded. - """ - - path = self.folderLoadDialog() - - # If no file was selected, do nothing - if path == '': - return - - try: - self.loadLayoutFromFile(path) - - # Anything goes wrong with loading the selected one, - # we dont care about what went wrong - except (OSError, JSONDecodeError): - self.loadDefaultLayoutToolBarButtonClick() - self.errorBox("Unable to load given file.") - else: - self.messageBox("File loaded successfully.") - - def errorBox(self, message="Something went wrong.") -> None: - """ - Displays a simple box with the given error message. - """ - - dlg = QMessageBox(self) - dlg.setWindowTitle("Error") - dlg.setText(message) - dlg.exec() - - def messageBox(self, message="Something happend.") -> None: + def messageBox(self, message, title="Message") -> None: """ Displays a simple box with the given message. """ - dlg = QMessageBox(self) - dlg.setWindowTitle("Message") + dlg.setWindowTitle(title) dlg.setText(message) dlg.exec() - def undoToolBarButtonClick(self) -> None: + def add_module_graphics_item(self, item: ModuleGraphicsItem) -> None: """ - Undos as many processor cycles as the number entered in the box. + Add a module graphics item to the graphics scene and connect its + QT signals and slots. """ - try: - steps = self.jump_value_box.value() - self.cpu.load_cycle(max(self.cpu.get_clock() - steps, 0)) - self.updateCpuListeners() - except (ValueError, IndexError): - self.errorBox("Unable to undo the cycle.") + self._graphics_scene.add_module(item) + self.connectModuleActions(item.getActionSignals()) + self._processor_handler.changed.connect(item.update) - def undoAsmToolBarButtonClick(self) -> None: + def add_all_signals(self) -> None: """ - Undos as many processor cycles as the number entered in the box. + Add visual representations of all processor signals between all modules + added to the GUI. """ - try: - steps = self.asm_jump_value_box.value() - self.cpu.undo_asm_instruction(steps) - self.updateCpuListeners() - except (ValueError, IndexError): - self.errorBox("Unable to undo the cycle.") + self._graphics_scene.add_all_signals() - def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None: - """ - Adds an item to the graphics scene. - """ - self.cpu_graphics_scene.addModuleGraphicsItem(graphics_item) - self.connectModuleActions(graphics_item.getActionSignals()) + def load_layout(self, file_path: str) -> None: + """Load a processor layout from file. + + Parameters + ---------- + file_path : str + Path to the file containing a processor layout to load. - def addAllSignals(self) -> None: - """ - Add signals depending on modules in the graphics scene. """ - self.cpu_graphics_scene.addAllSignals() + self._graphics_scene.load_layout_from_file(file_path) if __name__ == '__main__': diff --git a/src/simudator/gui/menu_bar.py b/src/simudator/gui/menu_bar.py new file mode 100644 index 0000000000000000000000000000000000000000..29183a9df0ef1db0a99dd035a3407b83e93eead9 --- /dev/null +++ b/src/simudator/gui/menu_bar.py @@ -0,0 +1,192 @@ +import typing + +from PyQt5.QtWidgets import QAction +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QMenuBar, QWidget + + +class MainMenuBar(QMenuBar): + """ + Main menu bar of the simudator GUI. Contains submenus for file I/O, + layout managing and miscellaneous tools. + + Parameters + ---------- + parent : QWidget | None + Parent of the widget. The widget will be a new window if it is ``None`` + (default). + """ + + load = pyqtSignal() + """ + QT signal emitted when the user has requested to load a processor state. + """ + + save = pyqtSignal() + """ + QT signal emitted when the user has requested to save the processor state. + """ + + reset = pyqtSignal() + """ + QT signal emitted when the user has request to reset the processor state. + """ + + load_layout = pyqtSignal() + """ + QT signal emitted when the user has request to load a processor layout. + """ + + load_default_layout = pyqtSignal() + """ + QT signal emitted when the user has request to load the default processor + layout. + """ + + save_layout = pyqtSignal() + """ + QT signal emitted when the user has request to save the processor layout. + """ + + lock_layout = pyqtSignal(bool) + """ + QT signal emitted when the user has request to toggle the lock of editing + the processor layout. + """ + + show_breakpoints = pyqtSignal() + """ + QT signal emitted when the user has requested to show and manage + the processor breakpoints. + """ + + show_all_signals = pyqtSignal(bool) + """ + QT signal emitted when the user has requested to toggle showing all + processor signals. + """ + + show_port_names = pyqtSignal(bool) + """ + QT signal emitted when the user has requested to toggle showing the names + of processor module ports. + """ + + update_all_values = pyqtSignal(bool) + """ + QT signal emitted when the user has requested to see all values of + processor modules update in real time when running the processor. + """ + + set_delay = pyqtSignal() + """ + QT signal emitted when the user has requested to set the delay between + clock cycles when running the processor. + """ + + def __init__(self, parent: typing.Optional[QWidget] = None) -> None: + super().__init__(parent) + + # Create load action + self._load_action = QAction("Load", self) + self._load_action.setStatusTip("Load processor state from file") + self._load_action.triggered.connect(self.load) + + # Create save action + self._save_action = QAction("Save", self) + self._save_action.setStatusTip("Save processor state to file") + self._save_action.triggered.connect(self.save) + + self._reset_action = QAction("Reset", self) + self._reset_action.setStatusTip("Reset processor") + self._reset_action.triggered.connect(self.reset) + + # Create File menu for load and save + file_menu = self.addMenu("&File") + file_menu.addAction(self._load_action) + file_menu.addAction(self._save_action) + file_menu.addAction(self._reset_action) + + # create load layout action + load_layout_action = QAction("Load layout", self) + load_layout_action.setStatusTip("Loads the selected layout from file.") + load_layout_action.triggered.connect(self.load_layout) + + # create load default layout action + load_default_layout_action = QAction("Load default layout", self) + load_default_layout_action.setStatusTip("Loads the default layout from file.") + load_default_layout_action.triggered.connect(self.load_default_layout) + + # create save layout action + save_layout_action = QAction("Save layout", self) + save_layout_action.setStatusTip("Saves the current layout to a file.") + save_layout_action.triggered.connect(self.save_layout) + + # create layout lock action + self._lock_layout_action = QAction("Lock layout", self, checkable=True) + self._lock_layout_action.setStatusTip("Lock layout so items can't be moved.") + self._lock_layout_action.triggered.connect(self.lock_layout) + + # Create show signals actions + self._signal_vis_action = QAction("Show signals", self, checkable=True) + self._signal_vis_action.setChecked(True) + self._signal_vis_action.setStatusTip("Toggle the visibility of signal.") + self._signal_vis_action.triggered.connect(self.show_all_signals) + + # Create show port name actions + self._port_vis_action = QAction("Show port names", self, checkable=True) + self._port_vis_action.setChecked(True) + self._port_vis_action.setStatusTip("Toggle the visibility of port names.") + self._port_vis_action.triggered.connect(self.show_port_names) + + # Create Layout menu for layout actions + layout_menu = self.addMenu("&Layout") + layout_menu.addAction(load_layout_action) + layout_menu.addAction(load_default_layout_action) + layout_menu.addAction(save_layout_action) + layout_menu.addAction(self._lock_layout_action) + layout_menu.addAction(self._signal_vis_action) + layout_menu.addAction(self._port_vis_action) + + # Create breakpoint window action + self._breakpoint_action = QAction("Breakpoints", self) + self._breakpoint_action.setStatusTip("Open breakpoint window.") + self._breakpoint_action.triggered.connect(self.show_breakpoints) + + # Create 'update value' window button + self._update_value_action = QAction( + "Update values while running", self, checkable=True + ) + self._update_value_action.setChecked(False) + self._update_value_action.setStatusTip("Toggle value updates while running.") + self._update_value_action.triggered.connect(self.update_all_values) + + # Create 'set delay' window button + self._set_delay_action = QAction("Set update delay", self) + self._set_delay_action.setStatusTip( + "Sets the delay between each update when the cpu is running." + ) + self._set_delay_action.triggered.connect(self.set_delay) + + # Create Tools menu for tool actions actions + tools_menu = self.addMenu("&Tools") + tools_menu.addAction(self._breakpoint_action) + tools_menu.addAction(self._update_value_action) + tools_menu.addAction(self._set_delay_action) + + @Slot(bool) + def set_disabled_when_running(self, value: bool) -> None: + """ + Toggle the menu bar between enabled and disabled. When disabled, + actions which should not be performed while a processor is running + are disabled and greyed out. + + Parameters + ---------- + value : bool + ``True`` to disable the menu bar, ``False`` to enable it. + """ + self._load_action.setDisabled(value) + self._save_action.setDisabled(value) + self._reset_action.setDisabled(value) diff --git a/src/simudator/gui/module_graphics_item/memory_graphic.py b/src/simudator/gui/module_graphics_item/memory_graphic.py index 563036d2ddb51ecdda681c6aad9cb83bf47c5954..c4a944d213270e0f99330c5dede4d914dfc2a611 100644 --- a/src/simudator/gui/module_graphics_item/memory_graphic.py +++ b/src/simudator/gui/module_graphics_item/memory_graphic.py @@ -340,7 +340,8 @@ class MemoryGraphicsItem(ModuleGraphicsItem): module_state = self.module.get_state() module_state['memory'][parsed_address] = value self.module.set_state(module_state) - self.update_graphics_signal.emit() + self.module_edited.emit() + self.update() def getActionSignals(self) -> []: # Do parent signals and then add memory specific signals diff --git a/src/simudator/gui/module_graphics_item/module_graphics_item.py b/src/simudator/gui/module_graphics_item/module_graphics_item.py index 30ae67a723d2f6c1fd73596e917d49a19c63b8d6..155567816c45c4e13061b8cd93931d2b9c0ddd5c 100644 --- a/src/simudator/gui/module_graphics_item/module_graphics_item.py +++ b/src/simudator/gui/module_graphics_item/module_graphics_item.py @@ -38,6 +38,8 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem): new_state_breakpoint_signal = pyqtSignal(str, str, str) update_graphics_signal = pyqtSignal() + module_edited = pyqtSignal() + def __init__(self, module: Module, name: str = None): super().__init__() @@ -190,8 +192,10 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem): module_state[state] = parsed_value self.module.set_state(module_state) # Since we have changed a value we send a signal to the gui to update - self.update_graphics_signal.emit() + self.module_edited.emit() + self.update() + @Slot() def update(self): """ Update the visuals of the graphics item to match it's module. diff --git a/src/simudator/gui/pipeline.py b/src/simudator/gui/pipeline.py index 16dbfc1261bf81b74e6b4aaaee42c2a42bc7f7e6..3ba4ffea08234d266b38e1b1526f6803c6059432 100644 --- a/src/simudator/gui/pipeline.py +++ b/src/simudator/gui/pipeline.py @@ -1,5 +1,6 @@ from typing import Any +from qtpy.QtCore import Slot from qtpy.QtWidgets import QTableWidget, QTableWidgetItem @@ -15,6 +16,7 @@ class PipeLine(QTableWidget): super().__init__() self.setEditTriggers(self.NoEditTriggers) + @Slot(list) def set_instructions(self, instructions: list[tuple[str, int, int]]) -> None: """ Give the pipeline the current CPU instructions. diff --git a/src/simudator/gui/processor_handler.py b/src/simudator/gui/processor_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..91612fd9a6e5af44e70f145e0745729c632d44a7 --- /dev/null +++ b/src/simudator/gui/processor_handler.py @@ -0,0 +1,335 @@ +from qtpy import QtCore +from qtpy.QtCore import QObject, QThreadPool +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QErrorMessage, QFileDialog, QInputDialog, QMessageBox + +from simudator.core.processor import Processor +from simudator.gui.run_continuously_thread import RunThread + + +class ProcessorHandler(QObject): + """ + Wrapper class for interacting with the processor in the GUI. + + Parameters + ---------- + processor : Processor + parent : QObject | None + Optional parent QObject that takes ownership of this QObject. + """ + + running = pyqtSignal(bool) + """ + PyQT signal emitted when the processor has started or finished running. + Emits ``True`` when it has started and ``False`` when it has finished. + """ + cycle_changed = pyqtSignal(int) + """ + PyQT signal emitted when the current clock cycle count of the processor + has changed. Emits the current clock cycle count as an ``int``. + """ + changed = pyqtSignal() + """ + PyQT signal emitted when the processor has changed state. + """ + changed_instruction = pyqtSignal(list) + """ + PyQT signal emitted when the processor has changed the current instruction(s) + being executed. Emits a list of instructions. + + See Also + -------- + simudator.core.processor.get_current_instructions : + Method of the processor used to get instructions that are emitted. + """ + + def __init__( + self, + processor: Processor, + parent: QObject | None = None, + ) -> None: + super().__init__(parent) + self._processor = processor + # Threadpool for running cpu and gui concurrently + self._threadpool = QThreadPool.globalInstance() + + # Used to set if all values in the gui should be updated each tick + # or only the clock counter + self._update_all_values = False + + # Used to set the update delay + # Useful when watching the values being updated while running + self._update_delay = 0.00 + + # Used to lock some actions in ui when cpu is running in another thread + # Using the cpu's internal status directly could case problems + self._processor_running = False + + self._error_msg_box = QErrorMessage() + + def _signal_processor_changed(self) -> None: + """ + Emit signals for signifying that the processor has changed its state. + """ + self.cycle_changed.emit(self._processor.get_clock()) + self.changed.emit() + self.changed_instruction.emit(self._processor.get_current_instructions()) + + @Slot(int) + def step_cycles(self, cycles: int): + """ + Run some number of clock cycles of the processor. + + Parameters + ---------- + Number of clock cycles to perform. + """ + + # Don't do steps if cpu is running + if self._processor_running: + return + + self._processor_running = True + self.running.emit(True) + simulation_thread = RunThread( + self._processor, + self._update_delay, + False, + False, + cycles, + ) + simulation_thread.processor_tick.connect(self.on_processor_tick) + self._threadpool.start(simulation_thread) + + @Slot(int) + def step_asm_instructions(self, instructions: int): + """ + Run some numer of asm instructions. + + Parameters + ---------- + instructions : int + Number of asm instructions to perform. + """ + + # Don't do steps if cpu is running + if self._processor_running: + return + + self._processor_running = True + self.running.emit(True) + simulation_thread = RunThread( + self._processor, self._update_delay, False, True, instructions + ) + simulation_thread.processor_tick.connect(self.on_processor_tick) + self._threadpool.start(simulation_thread) + + @Slot() + def run_simulation(self) -> None: + """ + Run the processor continuously until it is stopped by user input, + reaches a breakpoint or halts on its own. + """ + + # Don't run if already running + if self._processor_running: + return + + # Create own thread for cpu simulation so gui dosent freeze + self._processor_running = True + self.running.emit(True) + simulation_thread = RunThread(self._processor, self._update_delay) + simulation_thread.processor_tick.connect(self.on_processor_tick) + self._threadpool.start(simulation_thread) + + @Slot(int) + def on_processor_tick(self, steps: int) -> None: + """ + Called from other thread after every cpu tick. + Will inform the user and update visuals. + """ + + # Update cpu clock counter every tick + self.cycle_changed.emit(self._processor.get_clock()) + + if self._update_all_values: + self._signal_processor_changed() + + # A signal of 0 steps signifies end of execution, i.e. the CPU has + # halted or run the specified amount of ticks + # => Enable the relevant parts of the GUI again + if steps == 0: + self._processor_running = False + self.running.emit(False) + self._signal_processor_changed() + + # Inform user of reached break point + if self._processor.breakpoint_reached: + QMessageBox.information( + self.parent(), + "SimuDator", + f"Reached breakpoint: {str(self._processor.last_breakpoint)}", + ) + + # Inform user of halt + if self._processor.should_halt(): + QMessageBox.information( + self.parent(), "SimuDator", "The processor halted." + ) + + @Slot() + def stop_simulation(self) -> None: + """ + Stop the processor. + """ + self._processor.stop() + + def get_save_path(self) -> str: + """ + Prompt the user for a file path meant for saving to file. Creates the + specified file if it does not exist. + """ + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + + return dialog.getSaveFileName()[0] + + def get_file_path(self) -> str: + """ + Prompt the user for a file path meant for loading the processor state. + """ + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + + return dialog.getOpenFileName()[0] + + @Slot() + def load_state(self) -> None: + """ + Load the processor state from selected file. + """ + + # not safe to load while cpu is running + if self._processor_running: + return + + path = self.get_file_path() + try: + self._processor.load_state_from_file(path) + except KeyError: + self._error_msg_box.showMessage("Could not load selected file.", "load_err") + except ValueError: + self._error_msg_box.showMessage("Selected file was empty.", "load_empty") + except FileNotFoundError: + # This error is triggered when no file was selected by + # by the user. The user should know they did not select a + # file and therefore there no need to display a box telling + # them that. + # self.messageBox("No valid file was selected.") + pass + else: + QMessageBox.information(None, "SimuDator", "Loaded file successfully.") + self._signal_processor_changed() + + @Slot() + def reset(self) -> None: + """ + Reset the processor to initial values and signal the processor state + change. + """ + + # not safe to reset while cpu running + if self._processor_running: + return + + answer = QMessageBox.question( + self.parent(), + "Reset Processor", + "Are you sure you want to reset the processor?", + ) + + if answer == QMessageBox.Yes: + self._processor.reset() + self._signal_processor_changed() + + @Slot() + def save_state(self) -> None: + """ + Save the state of the processor to file. + + This erases the previous content of the file before saving the data. + """ + + # Not safe to save while running + if self._processor_running: + return + + path = self.get_save_path() + + # Change file to the selected file if a file was selected + if path == '': + return + + res = self._processor.save_state_to_file(path) + + if res: + QMessageBox.information(self.parent(), "SimuDator", "File saved.") + else: + self._error_msg_box.showMessage("Unable to save.", "save_err") + + @Slot(int) + def undo_cycles(self, cycles: int) -> None: + """ + Undo some number of processor clock cycles. + + Parameters + ---------- + cycles : int + Number of clock cycles to undo. + """ + try: + self._processor.load_cycle(max(self._processor.get_clock() - cycles, 0)) + self._signal_processor_changed() + except (ValueError, IndexError): + self._error_msg_box.showMessage("Unable to undo the cycle.") + + @Slot(int) + def undo_asm_instructions(self, instructions: int) -> None: + """ + Undo some number of asm instructions. + + Parameters + ---------- + instructions : int + Number of instructions to undo. + """ + try: + self._processor.undo_asm_instruction(instructions) + self._signal_processor_changed() + except (ValueError, IndexError): + self._error_msg_box("Unable to undo the instruction.") + + @Slot() + def toggle_value_update_on_run(self): + """ + Toggle whether all values in the GUI related to the processor should be + updated after each clock cycle or after having run several cycles at a + time / running continuously. The clock cycle counter is always updated + after each tick regardless. + """ + self._update_all_values = not self._update_all_values + + @Slot() + def set_update_delay(self): + """ + Set the delay waited between each consecutive clock cycle when running + several cycles or continuously. Prompts the user for the delay. + """ + delay, ok = QInputDialog.getDouble( + None, "Input Dialog", "Enter a float value:", decimals=5 + ) + if ok: + self._update_delay = delay diff --git a/src/simudator/gui/run_continuously_thread.py b/src/simudator/gui/run_continuously_thread.py index 72d0fdcbb58e3352901f21c5fca428a17200f2d4..13368de4f10407cce26c1fb8cf2bb1bc46c10186 100644 --- a/src/simudator/gui/run_continuously_thread.py +++ b/src/simudator/gui/run_continuously_thread.py @@ -1,39 +1,54 @@ import time -from qtpy.QtCore import QRunnable +from qtpy.QtCore import QObject, QRunnable +from qtpy.QtCore import Signal as pyqtSignal + + +class SignalHolder(QObject): + """ + Helper class for RunThread as the thread cannot inherit from QObject to + have signals directly. Instead, an instance of this class is used to hold + signals for the thread. + """ + + processor_tick = pyqtSignal(int) + class RunThread(QRunnable): """ - This class is used to run the simulated cpu several ticks or continuously on - a seperate thread. This allows the user to interact with the GUI while the - simulation is running. + This class is used to run the simulated cpu several ticks or continuously + on a separate thread. This allows the user to interact with the GUI while + the simulation is running. - After each CPU tick, this thread will emit to its given QT signal so that - the GUI can update itself and possibly inform the user of when the execution + After each CPU tick, this thread will emit to its QT signal so that the GUI + can update itself and possibly inform the user of when the execution has halted. """ - def __init__(self, cpu, signal, delay: float, run_continuously=True, - step_asm=False, steps=0): + def __init__( + self, cpu, delay: float, run_continuously=True, step_asm=False, steps=0 + ): + super().__init__() self.cpu = cpu - self.signal = signal self.run_continuously = run_continuously self.steps = steps self.delay = delay self.step_asm = step_asm + self.signals = SignalHolder() + self.processor_tick = self.signals.processor_tick def run(self): if self.step_asm: while self.steps > 0: self.cpu.do_tick() + self.processor_tick.emit(1) # We only care about asm instructions if self.cpu.is_new_instruction(): self.steps -= 1 - self.signal.emit(1) time.sleep(self.delay) if self.cpu.is_stopped: @@ -44,18 +59,17 @@ class RunThread(QRunnable): self.cpu.unstop() while not self.cpu.is_stopped: self.cpu.do_tick() - self.signal.emit(1) + self.processor_tick.emit(1) time.sleep(self.delay) else: for _ in range(self.steps): self.cpu.do_tick() - self.signal.emit(1) + self.processor_tick.emit(1) time.sleep(self.delay) if self.cpu.is_stopped: break - # Signal end of execution as having run 0 ticks - self.signal.emit(0) + self.processor_tick.emit(0) diff --git a/src/simudator/gui/simulation_toolbar.py b/src/simudator/gui/simulation_toolbar.py new file mode 100644 index 0000000000000000000000000000000000000000..090627eaae00362cee96f3aa2f61159b6fc9faf7 --- /dev/null +++ b/src/simudator/gui/simulation_toolbar.py @@ -0,0 +1,193 @@ +from PyQt5.QtWidgets import QAction +from qtpy import QtGui +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QLabel, QSpinBox, QStyle, QToolBar, QWidget + + +class SimulationToolBar(QToolBar): + """ + A toolbar for basic controls of a processor simulation. This includes + running and stopping the processor, stepping and undoing clock cycles and + stepping and undoing asm instructions. + + Parameters + ---------- + title : str + Title of the toolbar. + parent : QWidget | None + Parent of the widget. The widget will be a new window if it is ``None`` + (default). + """ + + run = pyqtSignal() + """ + QT signal emitted when the user has requested to run the processor + continuously. + """ + + stop = pyqtSignal() + """ + QT signal emitted when the user has requested to stop the processor. + """ + + step = pyqtSignal(int) + """ + QT signal emitted when the user has requested to step some clock cycles + of the processor. + """ + + undo = pyqtSignal(int) + """ + QT signal emitted when the user has requested to undo some clock cycles of + the processor. + """ + + step_asm = pyqtSignal(int) + """ + QT signal emitted when the user has requested to step some asm instructions + of the processor. + """ + + undo_asm = pyqtSignal(int) + """ + QT signal emitted when the user has requested to undo some asm instructions + of the processor. + """ + + def __init__(self, title, parent: QWidget | None = None): + super().__init__(title, parent) + # Run continuously button + arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay) + self._run_action = QAction(arrow_icon, "Run", self) + self._run_action.setStatusTip("Run until halt") + self._run_action.triggered.connect(self.run.emit) + self.addAction(self._run_action) + + # Stop button + stop_icon = self.style().standardIcon(QStyle.SP_MediaStop) + self._stop_action = QAction(stop_icon, "Stop", self) + self._stop_action.setStatusTip("Stop running") + self._stop_action.triggered.connect(self.stop.emit) + self.addAction(self._stop_action) + + # Separator and spacing between basic controls and stepping clock cycles + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + self.addSeparator() + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + + # Clock cycle counter + self._clock_cycle_label = QLabel("Clock cycles: 0", self) + self._clock_cycle_label.setMinimumWidth(150) + self.addWidget(self._clock_cycle_label) + + # Undo clock cycle button + backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) + self._undo_action = QAction(backward_arrow_icon, "Undo", self) + self._undo_action.setStatusTip("Undo the last processor tick") + self._undo_action.triggered.connect(self._signal_undo) + self.addAction(self._undo_action) + + # Step clock cycle button + forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) + self._step_action = QAction(forward_arrow_icon, "Step", self) + self._step_action.setStatusTip("Run one clock cycle") + self._step_action.triggered.connect(self._signal_step) + self.addAction(self._step_action) + + # Spin box for the number of clock cycles to step/undo at a time + self._jump_value_box = QSpinBox() + self._jump_value_box.setMinimum(999999) + self._jump_value_box.setMinimum(1) + self._jump_value_box.setValue(1) + self.addWidget(self._jump_value_box) + + # Separator and spacing between steping clock cycles and + # stepping asm instructions + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + self.addSeparator() + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + + # Asm instruction counter + self._asm_label = QLabel("Assembler instructions: ", self) + self.addWidget(self._asm_label) + + # Undo asm instruction button + backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) + self._undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self) + self._undo_asm_action.setStatusTip("Undo the last assembler instruction") + self._undo_asm_action.triggered.connect(self._signal_undo_asm) + self.addAction(self._undo_asm_action) + + # Step asm instruction button + forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) + self._step_asm_action = QAction(forward_arrow_icon, "Step Asm", self) + self._step_asm_action.setStatusTip("Run one assembler instruction") + self._step_asm_action.triggered.connect(self._signal_step_asm) + self.addAction(self._step_asm_action) + + # Spin box for the number of asm instructions to step/undo at a time + self._asm_jump_value_box = QSpinBox() + self._asm_jump_value_box.setMinimum(999999) + self._asm_jump_value_box.setMinimum(1) + self._asm_jump_value_box.setValue(1) + self.addWidget(self._asm_jump_value_box) + + def contextMenuEvent(self, a0: QtGui.QContextMenuEvent | None) -> None: + # Override to remove the functionality of hiding the toolbar as there + # is currently no way of showing it again + pass + + @Slot(int) + def update_clock(self, clock_cycle: int) -> None: + """ + Set the displayed number of the clock cycle counter. + + Parameters + ---------- + clock_cycle : int + Number of clock cycles to display. + """ + self._clock_cycle_label.setText(f"Clock cycles: {clock_cycle}") + + @Slot(bool) + def set_disabled_when_running(self, value: bool) -> None: + """ + Disable relevant parts of the toolbar related to controlling a + processor. Intended to be used to disable stepping functionality + while the processor is running. + + All buttons except for the stop button are disabled. The disabled + buttons are greyed out and cannot be interacted with while the toolbar + is disabled. + + Parameters + ---------- + value : bool + ``True`` to disable the toolbar, ``False`` to enable it. + """ + self._run_action.setDisabled(value) + self._step_action.setDisabled(value) + self._undo_action.setDisabled(value) + self._step_asm_action.setDisabled(value) + self._undo_asm_action.setDisabled(value) + + def _signal_step(self): + self.step.emit(self._jump_value_box.value()) + + def _signal_undo(self): + self.undo.emit(self._jump_value_box.value()) + + def _signal_step_asm(self): + self.step_asm.emit(self._asm_jump_value_box.value()) + + def _signal_undo_asm(self): + self.undo_asm.emit(self._asm_jump_value_box.value()) diff --git a/src/simudator/gui/view.py b/src/simudator/gui/view.py new file mode 100644 index 0000000000000000000000000000000000000000..08e1b3535b69629ce2fa6227024ce9e95e71cb43 --- /dev/null +++ b/src/simudator/gui/view.py @@ -0,0 +1,49 @@ +from qtpy import QtCore, QtWidgets +from qtpy.QtGui import QWheelEvent +from qtpy.QtWidgets import QGraphicsView + + +class View(QGraphicsView): + """ + This class views all QGraphicsItems for the user. It takes the + QGraphicsScene as input and inherits all funcitonality from the + QGraphicsView and overrides the wheelEvent function. + This allows the users to navigate the view with their trackpads + and zoom in/out with their trackpads + ctrl. + """ + + def __init__(self, QGraphicsScene): + super().__init__(QGraphicsScene) + self.scene = QGraphicsScene + + def wheelEvent(self, event: QWheelEvent | None) -> None: + """ + Default behaviour if ctrl is not pressed, otherwise zoom in/out. + """ + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ControlModifier: + # Factor above 1 zooms in, below zooms out + factor = 1.03 + if event.angleDelta().y() < 0: + factor = 0.97 + + # If event got triggered due to the x axis, do nothing + if event.pixelDelta().x() != 0: + return + + view_pos = event.globalPosition() + scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y())) + + self.centerOn(scene_pos) + self.scale(factor, factor) + + old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y())) + new_mapToScene = self.mapToScene(self.viewport().rect().center()) + + delta = old_mapToScene - new_mapToScene + + self.centerOn(scene_pos - delta) + + else: + # Default behaviour + super().wheelEvent(event) diff --git a/src/simudator/processor/mia/mia.py b/src/simudator/processor/mia/mia.py index 536b2b0dfcc186ad58abe587479ca9b93913d622..b90e2f5efdc0b4514ea70a73595531ab1905b540 100644 --- a/src/simudator/processor/mia/mia.py +++ b/src/simudator/processor/mia/mia.py @@ -345,36 +345,36 @@ class MIA_CPU(Processor): (grx.signals["in_input"], grx.signals["out_content"]), ] - gui.addModuleGraphicsItem( + gui.add_module_graphics_item( BusGraphicsItem(self.get_module("Bus"), bus_signal_pairs) ) - gui.addModuleGraphicsItem(uPcGraphicsItem(self.get_module("uPC"))) - gui.addModuleGraphicsItem(SupcGraphicsItem(self.get_module("SuPC"))) - gui.addModuleGraphicsItem(PcGraphicsItem(self.get_module("PC"))) - gui.addModuleGraphicsItem(ArGraphicsItem(self.get_module("AR"))) - gui.addModuleGraphicsItem(AsrGraphicsItem(self.get_module("ASR"))) - gui.addModuleGraphicsItem(HrGraphicsItem(self.get_module("HR"))) - gui.addModuleGraphicsItem(MicroMemoryGraphicsItem(self.get_module("uM"))) - gui.addModuleGraphicsItem(AluGraphicsItem(self.get_module("ALU"))) - gui.addModuleGraphicsItem(GrxGraphicsItem(self.get_module("GRx"))) - gui.addModuleGraphicsItem(IrGraphicsItem(self.get_module("IR"))) + gui.add_module_graphics_item(uPcGraphicsItem(self.get_module("uPC"))) + gui.add_module_graphics_item(SupcGraphicsItem(self.get_module("SuPC"))) + gui.add_module_graphics_item(PcGraphicsItem(self.get_module("PC"))) + gui.add_module_graphics_item(ArGraphicsItem(self.get_module("AR"))) + gui.add_module_graphics_item(AsrGraphicsItem(self.get_module("ASR"))) + gui.add_module_graphics_item(HrGraphicsItem(self.get_module("HR"))) + gui.add_module_graphics_item(MicroMemoryGraphicsItem(self.get_module("uM"))) + gui.add_module_graphics_item(AluGraphicsItem(self.get_module("ALU"))) + gui.add_module_graphics_item(GrxGraphicsItem(self.get_module("GRx"))) + gui.add_module_graphics_item(IrGraphicsItem(self.get_module("IR"))) flag_modules = ["Z-Flag", "N-Flag", "C-Flag", "O-Flag", "L-Flag", "LC"] for name in flag_modules: module = self.get_module(name) widget = FlagGraphicsItem(module) - gui.addModuleGraphicsItem(widget) + gui.add_module_graphics_item(widget) memory_modules = ["PM", "K1", "K2"] for name in memory_modules: module = self.get_module(name) widget = MiaMemoryGraphicsItem(module) - gui.addModuleGraphicsItem(widget) + gui.add_module_graphics_item(widget) - gui.addAllSignals() + gui.add_all_signals() gui.show() - gui.loadLayoutFromFile("mia_layout") + gui.load_layout("mia_layout") app.exec() def launch_cli(self):