From 6a884d7bfb58f785633db4d486d32b269df9847d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20H=C3=B6gstedt?= <marin.hogstedt@hotmail.com>
Date: Tue, 25 Jun 2024 14:35:30 +0200
Subject: [PATCH] for some reason pyqt:s setItem does not work any more. The
 problam most likely has something to do with how qt signaling system works
 when the values in the QTable is updated. When it worked it was important
 that we called the update() function before connecting it to the signal so I
 thought that removing the inheritence would fix the issue. Since the parent
 connects the signal before the child class can call its update function. This
 changed nothing and the QTable is just broken. I do not understand why. It
 also could have something to do with the spaghetti code that is the general
 GUI code since the constructor is called 4 times when it should only be
 called once.

---
 .../module_graphics_item/memory_graphic.py    | 410 +++++++++++++++---
 .../processor/mia/gui/mia_memory_graphic.py   |   4 +-
 2 files changed, 343 insertions(+), 71 deletions(-)

diff --git a/src/simudator/gui/module_graphics_item/memory_graphic.py b/src/simudator/gui/module_graphics_item/memory_graphic.py
index e01531a..00b87cd 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 8ad3759..0e0a939 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:
-- 
GitLab