diff --git a/src/simudator/gui/dialogs/memory_content_dialog.py b/src/simudator/gui/dialogs/memory_content_dialog.py index 4bbeb63196470fbe1de36d5b53ecf1109531cb7e..732a564fc18895783810d94dfe09a43134709dc9 100644 --- a/src/simudator/gui/dialogs/memory_content_dialog.py +++ b/src/simudator/gui/dialogs/memory_content_dialog.py @@ -1,65 +1,281 @@ -from typing import Optional +from math import ceil from qtpy.QtCore import Qt from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtWidgets import ( - QComboBox, + QButtonGroup, QDialog, + QErrorMessage, QHBoxLayout, - QLineEdit, - QPushButton, + QHeaderView, + QRadioButton, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, QWidget, ) -from simudator.core import Module +from simudator.core.modules import Memory +from simudator.gui.formatting import Format, FormatInfo, format_to_str + + +class MemoryTable(QTableWidget): + """ + A class showing the contents of a memory module in a QTableWidget. + + This class assumes that the size of the memory module will remain constant. + + Parameters + ---------- + memory_module: Memory + An instance of the Memory base class. + column_size: int + An integer specifying the number of columns, optional. + format_info : FormatInfo + Optional formatter for formatting each cell in the memory table. + + """ + + state_edited = pyqtSignal() + + def __init__( + self, + memory_module: Memory, + format_info: FormatInfo | None = None, + column_size=-1, + ): + + super().__init__() + self._format_info = format_info + self._memory = memory_module + self._column_size = column_size + self._memory_size = len(self._memory.get_state()["memory"]) + self._set_column_size() + self.setColumnCount(self._column_size) + rows = ceil(self._memory_size / self._column_size) + self.setRowCount(rows) + self._init_table_items(self._column_size, rows) + self.setHorizontalHeaderLabels(["+" + str(i) for i in range(4)]) + self.set_editable(False) + self._errorMessageWidget = QErrorMessage() + + vertical_headers = [] + for i in range(0, self._memory_size, self._column_size): + vertical_headers.append(str(hex(i))) + + self.setVerticalHeaderLabels(vertical_headers) + + self._tt = 0 + self.update() + + # Signal used to detect when the user has edited a cell + # This code can only be called after the initial self.update(). + # When the QTableWidget is initialized it currently fills each celll with empty strings. + # It also signals cellChanged for each cell it initializes. Thus _on_cell_changed + # is called for each cell and tries to save the cells empty string into the modules memory as + # an integer. + self.cellChanged.connect(self._on_cell_changed) + + def update(self): + """ + Update the content of this widget to reflect the content of the memory module. + """ + print(self._tt) + self._tt += 1 + memory_content = self._memory.get_state()["memory"] + for i in range(self._memory_size): + value = memory_content[i] + row = i // self._column_size + col = i % self._column_size + + if self._format_info: + value = format_to_str(self._format_info, value) + print("-" * 20) + print(self._format_info.selected_format) + print(value) + print("-" * 20) + else: + value = str(value) + + if value != "": + self.set_item(row, col, value) + + def set_item(self, row: int, col: int, text: str) -> None: + """Set the text at specified table cell to the given text. + + Parameters + ---------- + row: int + The items row position in the table. + col: int + The items column position in the table. + text: str + The text to be displayed. + """ + item = QTableWidgetItem(text) + + self.setItem(row, col, item) + + def set_editable(self, editable: bool) -> None: + """ + Set the text to be editable on ``True``, uneditable on ``False``. + + Parameters + ---------- + editable: bool + Sets the text to be editable on ``True``, uneditable on ``False``. + + """ + if editable: + self.setEditTriggers(self.AllEditTriggers) + else: + self.setEditTriggers(self.NoEditTriggers) + + def _init_table_items(self, cols: int, rows: int) -> None: + for col in range(cols): + for row in range(rows): + item = QTableWidgetItem() + self.setItem(row, col, item) + + def _set_column_size(self) -> None: + """ + Set the column size to a reasonable value if the size was not given to the constructor. + + This function assumes that the attributes `column_size` and `memory_size` are set before it is called. + """ + + if not self._column_size == -1: + return + + if self._memory_size > 200: + self._column_size = 4 + return + + if self._memory_size > 100: + self._column_size = 2 + return + + self._column_size = 1 + return + + def _on_cell_changed(self, row, col): + state = self._memory.get_state() + item = self.item(row, col) + value = item.text() + if value == "": + return + index = row * self._column_size + col + state['memory'][index] = value + self._memory.set_state(state) + self.state_edited.emit() class MemoryContentDialog(QDialog): - accepted = pyqtSignal(str, str, str, name="accepted") + """ + A dialog for letting the user edit the state of a processor modules memory. + + Parameters + ---------- + module : Module + The module to edit the memory of. + parent : QWidget | None + Optional parent widget of this dialog. + flags : Qt.WindowFlags | Qt.WindowType + Optional window flags for the window of the dialog. + format_info : FormatInfo + Optional formatter for formatting each cell in the memory table. + """ + + state_edited = pyqtSignal() def __init__( self, - memory: Module, - parent: Optional['QWidget'] = None, + memory_module: Memory, + parent: QWidget | None = None, flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(), + format_info: FormatInfo | None = None, ) -> None: super().__init__(parent, flags) + self._memory_module = memory_module + self._format_info = format_info + + self._add_layout() + self._add_edit_buttons() + self._add_formatting_buttons() + + # Sets the size of each column to the smallest possible width that allows all content in each box to be visible self._memory_table.horizontalHeader().setSectionResizeMode( + self._memory_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeToContents + ) + self._memory_table.update() + + def _add_layout(self): + + # Create a layout that expands vertically + # so that the buttons can be displayed below the + # memory content + self._layout = QVBoxLayout(self) + self._memory_table = MemoryTable(self._memory_module, self._format_info) + self._memory_table.state_edited.connect(self.state_edited) + self._layout.addWidget(self._memory_table) + + def _add_edit_buttons(self): + + # Create edit/view buttons, they are exclusive by default + self._edit_button = QRadioButton("Edit") + self._view_button = QRadioButton("View") + self._edit_group = QButtonGroup() + self._edit_group.addButton(self._edit_button, 1) + self._edit_group.addButton(self._view_button, 2) + + # Connect them to the 'set_edit' function + self._edit_group.buttonClicked.connect(self._set_edit) + self._edit_button_layout = QHBoxLayout() + self._edit_button_layout.addWidget(self._edit_button) + self._edit_button_layout.addWidget(self._view_button) + self._layout.addLayout(self._edit_button_layout) + + # Set the memory to be uneditable on startup + self._view_button.toggle() + + def _add_formatting_buttons(self): - self.memory = memory - self.memory_name = memory.get_state()['name'] + # If format info is 'None' do nothing + if not self._format_info: + return - adresses = [str(i) for i in range(len(memory.get_state()['memory']))] + # If there are no supported_formats we want to avoid creating + # a new button group since it will take up space that is not used + if len(self._format_info.supported_formats) == 0: + return - self.adressSelectWidget = QComboBox() - self.adressSelectWidget.addItems(adresses) - self.adressSelectWidget.activated.connect(self.updateValue) - self.adressSelectWidget.setEditable(True) + self._format_group = QButtonGroup() + self._format_button_layout = QHBoxLayout() + format_names = [format.name for format in self._format_info.supported_formats] - self.valuesWidget = QLineEdit() + # Create formatting buttons, they are exclusive by default + # Each button will be mapped to the value of the enums + for format in Format: - okButton = QPushButton('OK') - okButton.clicked.connect(self.signalAccepted) - cancelButton = QPushButton('Cancel') - cancelButton.clicked.connect(self.close) + name = format.name + id = format.value[0] - hboxLayout = QHBoxLayout() - hboxLayout.addWidget(self.adressSelectWidget, 0) - hboxLayout.addWidget(self.valuesWidget, 1) - hboxLayout.addWidget(okButton) - hboxLayout.addWidget(cancelButton) - self.setLayout(hboxLayout) + if name in format_names: + button = QRadioButton(name) + self._format_group.addButton(button, id) + self._format_button_layout.addWidget(button) - self.show() - self.updateValue() + self._layout.addLayout(self._format_button_layout) + self._format_group.buttonClicked.connect(self._set_format) - def updateValue(self) -> None: - selectedAdress = int(self.adressSelectWidget.currentText(), 16) - memory_content = self.memory.get_state()['memory'] - value = memory_content[selectedAdress] - self.valuesWidget.setText(str(value)) + def _set_edit(self): + pressed_button_id = self._edit_group.checkedId() + if pressed_button_id == 1: + self._memory_table.set_editable(True) + if pressed_button_id == 2: + self._memory_table.set_editable(False) - def signalAccepted(self) -> None: - adress = self.adressSelectWidget.currentText() - enteredValues = self.valuesWidget.text() - self.accepted.emit(self.memory_name, adress, enteredValues) - self.close() + def _set_format(self): + pressed_button_id = self._format_group.checkedId() + self._format_info.selected_format = pressed_button_id + self._memory_table.update() diff --git a/src/simudator/gui/dialogs/module_state_dialog.py b/src/simudator/gui/dialogs/module_state_dialog.py index fc768d7ade04490453e641d1f77fc3e2d5a76266..7f9399b9bb097c0ed0e826056711c800bbae2823 100644 --- a/src/simudator/gui/dialogs/module_state_dialog.py +++ b/src/simudator/gui/dialogs/module_state_dialog.py @@ -104,4 +104,3 @@ class ModuleStateDialog(QDialog): except ValueError as e: # TODO: error handling pass - diff --git a/src/simudator/gui/module_graphics_item/actions.py b/src/simudator/gui/module_graphics_item/actions.py index c1abb1f5f37d1f974e2816e03fe39a8af5e1a6c8..c4334a66b1feecf7a9a4dcdd719fce139493fb57 100644 --- a/src/simudator/gui/module_graphics_item/actions.py +++ b/src/simudator/gui/module_graphics_item/actions.py @@ -3,7 +3,9 @@ from enum import Enum, auto from PyQt5.QtWidgets import QAction from simudator.core.module import Module +from simudator.gui.dialogs.memory_content_dialog import MemoryContentDialog from simudator.gui.dialogs.module_state_dialog import ModuleStateDialog +from simudator.gui.formatting import FormatInfo from simudator.gui.module_graphics_item.module_widget import ModuleWidget @@ -14,6 +16,7 @@ class ActionType(Enum): """ EditState = (auto(),) + EditMemory = (auto(),) def edit_state_action(module: Module, module_widget: ModuleWidget) -> QAction: @@ -41,6 +44,42 @@ def edit_state_action(module: Module, module_widget: ModuleWidget) -> QAction: def action_triggered(): dialog = ModuleStateDialog(module, module_widget) dialog.state_edited.connect(module_widget.state_changed) + dialog.exec() + + action.triggered.connect(action_triggered) + return action + + +def edit_memory_action( + module: Module, module_widget: ModuleWidget, format_info: FormatInfo | None = None +) -> QAction: + """ + Create a QAction for opening a memory-edit dialog for a module widget. + The dialog triggers the state_changed signal of the module widget if the + user edites the contentet in the dialog. + + Parameters + ---------- + module : Module + The module that should be edited when triggering the created edit action. + module_widget : ModuleWidget + The module widget for the module that should be edited. Used for + displaying and parsing information correctly. + format_info : FormatInfo + Optional formatter for formatting each cell in the memory table. + + Returns + ------- + QAction + A Qt action for opening a dialog for editing the state of a modules memory. + Intended to be added as an action to the given module widget. + """ + action = QAction("Edit memory", module_widget) + + def action_triggered(): + dialog = MemoryContentDialog(module, format_info=format_info) + dialog.state_edited.connect(module_widget.state_changed) + dialog.exec() action.triggered.connect(action_triggered) return action @@ -64,4 +103,3 @@ def toggle_ports_action(module_widget: ModuleWidget) -> QAction: action = QAction("Toggle ports", module_widget) action.triggered.connect(module_widget.toggle_ports) return action - diff --git a/src/simudator/gui/module_graphics_item/integer_memory_graphic.py b/src/simudator/gui/module_graphics_item/integer_memory_graphic.py index 4902f59be8670a117cfd8665d1385ce16462e656..4e4ea6bbf40c22d2702698b26682dc8c52cbe6dd 100644 --- a/src/simudator/gui/module_graphics_item/integer_memory_graphic.py +++ b/src/simudator/gui/module_graphics_item/integer_memory_graphic.py @@ -1,5 +1,3 @@ -import sys -import traceback from enum import Enum from qtpy.QtWidgets import QButtonGroup, QHBoxLayout, QHeaderView, QRadioButton diff --git a/src/simudator/gui/processor_handler.py b/src/simudator/gui/processor_handler.py index e0ccb9ac0f5fe249540a5304ea10ad4cb149dff3..9bf8937d51a031f8f2fff4501e1915036b46295a 100644 --- a/src/simudator/gui/processor_handler.py +++ b/src/simudator/gui/processor_handler.py @@ -340,4 +340,3 @@ class ProcessorHandler(QObject): by the user. """ pass - diff --git a/src/simudator/processor/mia/mia.py b/src/simudator/processor/mia/mia.py index eb52e58dabea5bc3e02b62ac1e12a4511046c7d5..8d771888ea1d16ad00b4626abc4eceb9a80fa668 100644 --- a/src/simudator/processor/mia/mia.py +++ b/src/simudator/processor/mia/mia.py @@ -4,6 +4,9 @@ from simudator.cli.cli import CLI from simudator.core.modules.integer_register import IntegerRegister from simudator.core.processor import Processor, Signal from simudator.gui.gui import GUI +from simudator.gui.module_graphics_item.actions import edit_memory_action, edit_state_action +from simudator.gui.module_graphics_item.module_widget import ModuleWidget +from simudator.gui.formatting import Format, FormatInfo from simudator.processor.mia.gui import ( AluGraphicsItem, ArGraphicsItem, @@ -384,6 +387,18 @@ class MIA_CPU(Processor): widget = MiaMemoryGraphicsItem(module) gui.add_module_graphics_item(widget) + pm = self.get_module("PM") + gui_pm = ModuleWidget(pm, {}, []) + action = edit_memory_action(pm, gui_pm, FormatInfo(FormatInfo.NUMERICAL_FORMATS, Format.Hex, bit_length=16)) + gui_pm.addAction(action) + gui.add_module_widget(gui_pm) + + ar = self.get_module("AR") + gui_ar = ModuleWidget(ar, {"value": FormatInfo(FormatInfo.NUMERICAL_FORMATS, Format.Hex, bit_length=16)}, []) + action = edit_state_action(ar, gui_ar) + gui_ar.addAction(action) + gui.add_module_widget(gui_ar) + gui.add_all_signals() gui.show()