diff --git a/src/simudator/gui/module_graphics_item/memory_graphic.py b/src/simudator/gui/module_graphics_item/memory_graphic.py index e01531a7881ca879657c7f9ee28e58020410a0e6..00b87cd12470f653e931db08a73c261635e994a5 100644 --- a/src/simudator/gui/module_graphics_item/memory_graphic.py +++ b/src/simudator/gui/module_graphics_item/memory_graphic.py @@ -9,13 +9,10 @@ from qtpy.QtWidgets import ( QAction, QButtonGroup, QErrorMessage, - QFrame, QGraphicsRectItem, QGraphicsSimpleTextItem, QHBoxLayout, QHeaderView, - QMainWindow, - QPushButton, QRadioButton, QTableWidget, QTableWidgetItem, @@ -31,35 +28,205 @@ from simudator.gui.port_graphics_item import PortGraphicsItem class Base(Enum): + NONE = 0 DECIMAL = 1 BINARY = 2 HEXADECIMAL = 3 -class TestWindow(QWidget): +class Sign(Enum): + NONE = 0 + Signed = 1 + Unsigned = 2 + + +class MemoryWindow(QWidget): """ A class showing the contents of a memory module in a new window. Parameters ---------- - memory_module: An instance of the Memory base class. + memory_module: Memory + An instance of the Memory base class. + column_size: int + The number of columns used in displaying the memory content, optional. """ - # Leaving this here if i close the tab by accident - # https://doc.qt.io/qtforpython-5/PySide2/QtWidgets/QButtonGroup.html#PySide2.QtWidgets.PySide2.QtWidgets.QButtonGroup - def __init__(self, memory_module: Memory): + def __init__(self, memory_module: Memory, column_size=-1): super().__init__() - # Create base buttons, they are exclusive by default - # so need a seperate QButtonGroup since these three - # have nothing to do with the edit/view buttons - self.decimal_button = QRadioButton("Decimal") - self.bin_button = QRadioButton("Binary") - self.hex_button = QRadioButton("Hexadecimal") - self.base_group = QButtonGroup() - self.base_group.addButton(self.decimal_button, 1) - self.base_group.addButton(self.bin_button, 2) - self.base_group.addButton(self.hex_button, 3) + # 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) + + self.edit_group.buttonClicked.connect(self._set_edit) + + # 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(memory_module, column_size) + self.layout.addWidget(self._memory_table) + + self.edit_layout = QHBoxLayout() + self.edit_layout.addWidget(self.edit_button) + self.edit_layout.addWidget(self.view_button) + self.layout.addLayout(self.edit_layout) + + # Set the default base to decimal and disable editing + # from start + self._set_edit() + + # 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( + QHeaderView.ResizeToContents + ) + + self.view_button.toggle() + self._memory_table.update() + + def _set_edit(self): + pressed_button_id = self.edit_group.checkedId() + if pressed_button_id == 1: + self._memory_table.make_table_editable() + if pressed_button_id == 2: + self._memory_table.make_table_uneditable() + + +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. + """ + + def __init__(self, memory_module: Memory, column_size=-1): + super().__init__() + 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.make_table_uneditable() + 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.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. + """ + 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 + + self.set_item(row, col, value) + + memory_content = self._memory.get_state()["memory"] + + 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 pipeline diagram. + col: int + The items column position in the pipeline diagram. + text: str + The text to be displayed. + """ + item = QTableWidgetItem(text) + + self.setItem(row, col, item) + + def make_table_uneditable(self): + self.setEditTriggers(self.NoEditTriggers) + + def make_table_editable(self): + self.setEditTriggers(self.AllEditTriggers) + + + 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() + + index = row * 4 + col + state['memory'][index] = value + self._memory.set_state(state) + +class IntegerMemoryWindow(QWidget): + """ + A class showing the contents of a memory module in a new window. + + Parameters + ---------- + memory_module: An instance of the Memory base class. + """ + + def __init__(self, memory_module: Memory, bit_length: int, column_size=-1): + super().__init__() # Create edit/view buttons, they are exclusive by default self.edit_button = QRadioButton("Edit") @@ -68,18 +235,59 @@ class TestWindow(QWidget): self.edit_group.addButton(self.edit_button, 1) self.edit_group.addButton(self.view_button, 2) - # Connect them to the 'set_base' function - self.base_group.buttonClicked.connect(self._set_base) - # Connect them to the 'set_base' function self.edit_group.buttonClicked.connect(self._set_edit) # Create a layout that expands vertically # so that the buttons can be displayed below the # memory content self.layout = QVBoxLayout(self) - self._memory_table = MemoryWindow(memory_module) + self._memory_table = MemoryTable(memory_module, column_size) self.layout.addWidget(self._memory_table) + self.edit_layout = QHBoxLayout() + self.edit_layout.addWidget(self.edit_button) + self.edit_layout.addWidget(self.view_button) + self.layout.addLayout(self.edit_layout) + + # Set the default base to decimal and disable editing + # from start + self._set_edit() + + # 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( + QHeaderView.ResizeToContents + ) + + self._memory_table = IntegerMemoryTable(memory_module, bit_length, column_size) + + self.bit_length = bit_length + + # Create base buttons, they are exclusive by default + # so need a seperate QButtonGroup since these three + # have nothing to do with the edit/view buttons + self.decimal_button = QRadioButton("Decimal") + self.bin_button = QRadioButton("Binary") + self.hex_button = QRadioButton("Hexadecimal") + + self.base_group = QButtonGroup() + self.base_group.addButton(self.decimal_button, 1) + self.base_group.addButton(self.bin_button, 2) + self.base_group.addButton(self.hex_button, 3) + + # Connect them to the 'set_base' function + self.base_group.buttonClicked.connect(self._set_base) + + self.signed_button = QRadioButton("Signed") + self.unsigned_button = QRadioButton("Unsigned") + + self.sign_group = QButtonGroup() + self.sign_group.addButton(self.signed_button, 1) + self.sign_group.addButton(self.unsigned_button, 2) + + # Connect them to the 'set_sign' function + self.sign_group.buttonClicked.connect(self._set_sign) + # Create a layout that expands horizontally, so that # all buttons appear in one row self.first_button_layout = QHBoxLayout() @@ -88,32 +296,29 @@ class TestWindow(QWidget): self.first_button_layout.addWidget(self.hex_button) self.layout.addLayout(self.first_button_layout) - self.second_button_layout = QHBoxLayout() - self.second_button_layout.addWidget(self.edit_button) - self.second_button_layout.addWidget(self.view_button) - self.layout.addLayout(self.second_button_layout) + # Create a layout that expands horizontally, so that + # all buttons appear in one row + self.sign_layout = QHBoxLayout() + self.sign_layout.addWidget(self.signed_button) + self.sign_layout.addWidget(self.unsigned_button) + self.layout.addLayout(self.sign_layout) - # Set the default base to decimal and disable editing - # from start + # Set the default base to decimal, and defaul sign to signed self.decimal_button.toggle() + self.signed_button.toggle() self.view_button.toggle() self._set_base() - self._set_edit() - - # 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( - QHeaderView.ResizeToContents - ) + self._set_sign() + self._memory_table.update() def _set_base(self): - base = Base.DECIMAL pressed_button_id = self.base_group.checkedId() + if pressed_button_id == 1: base = Base.DECIMAL - if pressed_button_id == 2: + elif pressed_button_id == 2: base = Base.BINARY - if pressed_button_id == 3: + else: base = Base.HEXADECIMAL self._memory_table.set_base(base) @@ -126,22 +331,39 @@ class TestWindow(QWidget): if pressed_button_id == 2: self._memory_table.make_table_uneditable() + def _set_sign(self): + pressed_button_id = self.sign_group.checkedId() -class MemoryWindow(QTableWidget): + if pressed_button_id == 1: + sign = Sign.Signed + else: + sign = Sign.Unsigned + + self._memory_table.set_sign(sign) + self._memory_table.update() + + +class IntegerMemoryTable(QTableWidget): """ - A class showing the contents of a memory module in a QTableWidget. + A class showing the contents of a memory module in a QTableWidget with a fixed bit length. This class assumes that the size of the memory module will remain constant. Parameters ---------- - memory_module: An instance of the Memory base class. - column_size: An integer specifying the number of columns, optional. - + memory_module: Memory + An instance of the Memory base class. + bit_length: int + An integer specifying the number of bits for each word. + column_size: int + An integer specifying the number of columns, optional. """ - def __init__(self, memory_module: Memory, column_size=-1): + def __init__(self, memory_module: Memory, bit_length: int, column_size=-1): super().__init__() + self._sign = Sign.NONE + self._base = Base.NONE + self._bit_length = bit_length self._memory = memory_module self._column_size = column_size self._memory_size = len(self._memory.get_state()["memory"]) @@ -152,13 +374,14 @@ class MemoryWindow(QTableWidget): self._init_table_items(self._column_size, rows) self.setHorizontalHeaderLabels(["+" + str(i) for i in range(4)]) self.make_table_uneditable() - self.base = None self._errorMessageWidget = QErrorMessage() + self.aaa = False vertical_headers = [] for i in range(0, self._memory_size, self._column_size): vertical_headers.append(str(hex(i))) + self.setVerticalHeaderLabels(vertical_headers) self.update() @@ -167,8 +390,8 @@ class MemoryWindow(QTableWidget): # 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. + # 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): @@ -176,16 +399,21 @@ class MemoryWindow(QTableWidget): Update the content of this widget to reflect the content of the memory module. """ 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.base == Base.DECIMAL: - value = str(int(value)) - if self.base == Base.BINARY: - value = str(bin(int(value))) - if self.base == Base.HEXADECIMAL: - value = str(hex(int(value))) + + if self._base == Base.DECIMAL: + value = self._cal_decimal_from_sign(int(value), self._sign) + elif self._base == Base.BINARY: + value = str(bin(int(value)))[2:] # Remove '0b' + elif self._base == Base.HEXADECIMAL: + value = str(hex(int(value)))[2:] # Remove '0x' + elif self._base == Base.NONE: + value = str(value) + self.set_item(row, col, value) def set_item(self, row: int, col: int, text: str) -> None: @@ -204,15 +432,18 @@ class MemoryWindow(QTableWidget): self.setItem(row, col, item) + def set_sign(self, sign: Sign): + self._sign = sign + + def set_base(self, base: Base): + self._base = base + def make_table_uneditable(self): self.setEditTriggers(self.NoEditTriggers) def make_table_editable(self): self.setEditTriggers(self.AllEditTriggers) - def set_base(self, base: Base): - self.base = base - def _init_table_items(self, cols: int, rows: int) -> None: for col in range(cols): for row in range(rows): @@ -240,38 +471,79 @@ class MemoryWindow(QTableWidget): self._column_size = 1 return + def _cal_decimal_from_sign(self, value: int, sign: Sign) -> str: + if sign == Sign.Unsigned: + return str(int(value)) + + threshold = 2 ** (self._bit_length - 1) + + # MSB is set to one, subtract that value + if value > threshold: + return str(value - threshold) + + # If its not set we can just return it + return str(value) + def _on_cell_changed(self, row, col): state = self._memory.get_state() + max_value = 2**self._bit_length item = self.item(row, col) value = item.text() - # value must be base 10 otherwise - # we run into issues with converting - # values when we change between bases + # Value should be base 10 + # It makes it easier to change between basis + # if we can assume we always change from base 10. + # Base should only be NONE when using the MemoryWindow + # class which should never be done in practice. try: - if self.base == Base.BINARY: + if self._base == Base.BINARY: value = int(value, 2) - elif self.base == Base.HEXADECIMAL: + elif self._base == Base.HEXADECIMAL: value = int(value, 16) - else: + elif self._base == Base.DECIMAL: value = int(value) + elif self._base == Base.NONE: + value = value + except ValueError: msg = None - if self.base == Base.BINARY: - msg = "You must enter a binary number preceeded by '0b' (e.g. 0b101)." - elif self.base == Base.DECIMAL: + if self._base == Base.BINARY: + msg = "You must enter a binary number (e.g. 101)." + elif self._base == Base.DECIMAL: msg = "You must enter a decimal number (e.g. 107)." - elif self.base == Base.HEXADECIMAL: - msg = ( - "You must enter a hexadecimal number preceeded by '0x' (e.g. 0xc3)." - ) + elif self._base == Base.HEXADECIMAL: + msg = "You must enter a hexadecimal number (e.g. c3)." self._errorMessageWidget.showMessage(msg) + + # Call update to "remove" the incorrect input self.update() return + value = int(value) + + if (abs(value)) > max_value: + self._errorMessageWidget.showMessage( + "The value is too large for the memory word length." + ) + + # Call update to "remove" the incorrect input + self.update() + return + + # Turn every number into its unsigned counterpart when + # viewing the number as a binary + # Does nothing if the value already is positive + + # Example: + # 4 bits => max value of 16 + # -7 = 1001 + # -7 + 16 % 16 = 16-7 = 9 = 1001 = -7 + + value = (value + max_value) % max_value + index = row * 4 + col state['memory'][index] = value self._memory.set_state(state) diff --git a/src/simudator/processor/mia/gui/mia_memory_graphic.py b/src/simudator/processor/mia/gui/mia_memory_graphic.py index 8ad375913b16a5bb22e62640f9364d33722f57d0..0e0a939710d0a94d6c6d113440ba97c65b851bd4 100644 --- a/src/simudator/processor/mia/gui/mia_memory_graphic.py +++ b/src/simudator/processor/mia/gui/mia_memory_graphic.py @@ -13,9 +13,9 @@ from qtpy.QtWidgets import ( from simudator.core.modules import Memory from simudator.gui.color_scheme import ColorScheme as CS from simudator.gui.module_graphics_item.memory_graphic import ( + IntegerMemoryWindow, MemoryGraphicsItem, MemoryWindow, - TestWindow, ) from simudator.gui.orientation import Orientation from simudator.gui.port_graphics_item import PortGraphicsItem @@ -73,7 +73,7 @@ class MiaMemoryGraphicsItem(MemoryGraphicsItem): Create and show a MemoryWindow that displays the contents of the memory module associated with this graphics item. """ - self.memory_window = TestWindow(self.module) + self.memory_window = IntegerMemoryWindow(self.module, 16) self.memory_window.show() def memoryBreakpointDialog(self) -> None: