From 869b0bdfb8e369f8e95881f1282f35c1f2f43c50 Mon Sep 17 00:00:00 2001
From: Johannes Kung <johku144@student.liu.se>
Date: Thu, 4 Jul 2024 15:46:19 +0200
Subject: [PATCH] Added a signal viewer graphics item

---
 src/simudator/gui/gui.py           | 20 ++++++-
 src/simudator/gui/signal_viewer.py | 84 ++++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+), 2 deletions(-)
 create mode 100644 src/simudator/gui/signal_viewer.py

diff --git a/src/simudator/gui/gui.py b/src/simudator/gui/gui.py
index 48297b3..a5a30b1 100644
--- a/src/simudator/gui/gui.py
+++ b/src/simudator/gui/gui.py
@@ -24,16 +24,18 @@ from qtpy.QtWidgets import (
 )
 
 from simudator.core.processor import Processor
+from simudator.core.signal import Signal
 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.module_graphics_item.module_graphics_item import ModuleGraphicsItem
 from simudator.gui.orientation import Orientation
+from simudator.gui.pipeline import PipeLine
 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
+from simudator.gui.signal_viewer import SignalViewer
 
 
 class View(QGraphicsView):
@@ -363,7 +365,7 @@ class GUI(QMainWindow):
                     signal.connect(self.updateCpuClockCycle)
 
     def init_pipeline(self) -> None:
-        """Initialize the pipeline diagram. 
+        """Initialize the pipeline diagram.
 
         Sets its height, width and instructions.
         """
@@ -987,6 +989,20 @@ class GUI(QMainWindow):
         """
         self.cpu_graphics_scene.addAllSignals()
 
+    def add_signal_viewer(self, signal: Signal, label: str | None = None) -> None:
+        """
+        Add a signal viewer to the graphics scene.
+
+        Parameters
+        ----------
+        signal : Signal
+            Processor signal to view.
+        label : str | None
+            Optional label of the signal viewer.
+        """
+        viewer = SignalViewer(signal, label)
+        self.cpu_graphics_scene.addItem(viewer)
+
 
 if __name__ == '__main__':
     QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
diff --git a/src/simudator/gui/signal_viewer.py b/src/simudator/gui/signal_viewer.py
new file mode 100644
index 0000000..3c00b00
--- /dev/null
+++ b/src/simudator/gui/signal_viewer.py
@@ -0,0 +1,84 @@
+from qtpy.QtCore import QPointF, QRectF, Qt
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtGui import QPainter, QPainterPath, QPainterPathStroker, QPalette, QPen
+from qtpy.QtWidgets import (
+    QGraphicsItem,
+    QGraphicsWidget,
+    QStyleOptionGraphicsItem,
+    QWidget,
+)
+
+from simudator.core.signal import Signal
+
+
+class SignalViewer(QGraphicsWidget):
+    DEFAULT_WIDTH = 50
+    DEFAULT_HEIGHT = 50
+
+    def __init__(
+        self,
+        signal: Signal,
+        label: str | None = None,
+        parent: QGraphicsItem | None = None,
+        flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
+    ) -> None:
+        super().__init__(parent, flags)
+        self._signal = signal
+        self._label = label
+        self._width: int = SignalViewer.DEFAULT_WIDTH
+        self._height: int = SignalViewer.DEFAULT_HEIGHT
+        self.outline_width: float = 1
+        self.setFlag(QGraphicsItem.ItemIsMovable)
+        self.setFiltersChildEvents(False)
+
+    def paint(
+        self,
+        painter: QPainter | None = None,
+        option: QStyleOptionGraphicsItem | None = None,
+        widget: QWidget | None = None,
+    ) -> None:
+        painter.save()
+
+        # Draw outline of the items shape with fill
+        palette: QPalette = option.palette
+        pen = QPen(palette.color(QPalette.ColorRole.Dark))
+        brush = palette.brush(QPalette.ColorRole.Base)
+        painter.setPen(pen)
+        painter.setBrush(brush)
+        painter.drawPath(self.shape())
+
+        # Draw label if any
+        value_height_offset = self._height / 2
+        if self._label is not None:
+            # TODO: Proper positioning on the y-axis
+            x_pos = self._width / 2 - len(self._label) * 3
+            text_pos = QPointF(x_pos, self._height / 3)
+            painter.setPen(palette.text().color())
+            painter.setBrush(palette.text())
+            painter.drawText(text_pos, self._label)
+            value_height_offset = self._height * 2 / 3
+
+        # Draw signal value
+        # TODO: Proper positioning on the y-axis
+        text_pos = QPointF(0, value_height_offset)
+        painter.setPen(palette.text().color())
+        painter.setBrush(palette.text())
+        painter.drawText(text_pos, str(self._signal.get_value()))
+
+        painter.restore()
+
+    @Slot()
+    def update(self, rect: QRectF | None = None):
+        super().update()
+
+    def shape(self) -> QPainterPath:
+        path = QPainterPath()
+        path.addRect(0, 0, self._width, self._height)
+        # outline_painter = QPainterPathStroker()
+        # outline_painter.setWidth(self.outline_width)
+        # return outline_painter.createStroke(path)
+        return path
+
+    def boundingRect(self) -> QRectF:
+        return QRectF(0, 0, self._width, self._height)
-- 
GitLab