From 594ab29331f47ecde384237f0e6308ffd6010a51 Mon Sep 17 00:00:00 2001
From: Oscar Gustafsson <oscar.gustafsson@gmail.com>
Date: Sat, 21 Jan 2023 16:38:02 +0100
Subject: [PATCH] Initial support to wrap operations during scheduling

---
 b_asic/scheduler_gui/graphics_axes_item.py    | 19 ++--
 .../scheduler_gui/graphics_component_item.py  | 37 +++----
 b_asic/scheduler_gui/graphics_graph_event.py  | 22 +++--
 b_asic/scheduler_gui/graphics_graph_item.py   |  6 +-
 .../scheduler_gui/graphics_timeline_item.py   |  4 +-
 b_asic/scheduler_gui/main_window.py           | 96 +++++++++++--------
 test/test_scheduler_gui.py                    | 15 +++
 7 files changed, 120 insertions(+), 79 deletions(-)

diff --git a/b_asic/scheduler_gui/graphics_axes_item.py b/b_asic/scheduler_gui/graphics_axes_item.py
index de660c26..1dc36e33 100644
--- a/b_asic/scheduler_gui/graphics_axes_item.py
+++ b/b_asic/scheduler_gui/graphics_axes_item.py
@@ -108,8 +108,10 @@ class GraphicsAxesItem(QGraphicsItemGroup):
 
     @property
     def width(self) -> int:
-        """Get or set the current x-axis width. Setting the width to a new
-        value will update the axes automatically."""
+        """
+        Get or set the current x-axis width. Setting the width to a new
+        value will update the axes automatically.
+        """
         return self._width
 
     # @width.setter
@@ -119,8 +121,10 @@ class GraphicsAxesItem(QGraphicsItemGroup):
 
     @property
     def height(self) -> int:
-        """Get or set the current y-axis height. Setting the height to a new
-        value will update the axes automatically."""
+        """
+        Get or set the current y-axis height. Setting the height to a new
+        value will update the axes automatically.
+        """
         return self._height
 
     # @height.setter
@@ -140,7 +144,7 @@ class GraphicsAxesItem(QGraphicsItemGroup):
 
     @property
     def event_items(self) -> List[QGraphicsItem]:
-        """Returnes a list of objects, that receives events."""
+        """Return a list of objects, that receives events."""
         return [self._x_ledger[-1]]
 
     def _register_event_item(self, item: QGraphicsItem) -> None:
@@ -150,7 +154,6 @@ class GraphicsAxesItem(QGraphicsItemGroup):
     def set_height(self, height: int) -> "GraphicsAxesItem":
         # TODO: implement, docstring
         raise NotImplementedError
-        return self
 
     def set_width(self, width: int) -> "GraphicsAxesItem":
         # TODO: docstring
@@ -205,7 +208,7 @@ class GraphicsAxesItem(QGraphicsItemGroup):
             index -= 1
             is_timeline = False
 
-        ## make a new x-tick
+        # make a new x-tick
         # x-axis scale line
         self._x_scale.insert(index, QGraphicsLineItem(0, 0, 0, 0.05))
         self._x_scale[index].setPen(self._base_pen)
@@ -249,7 +252,7 @@ class GraphicsAxesItem(QGraphicsItemGroup):
         self.addToGroup(self._x_ledger[index])
         self._x_ledger[index].stackBefore(self._x_axis)
 
-        ## expand x-axis and move arrow,x-axis label, last x-scale, last x-scale-label
+        # expand x-axis and move arrow,x-axis label, last x-scale, last x-scale-label
         if not is_timeline:
             # expand x-axis, move arrow and x-axis label
             self._x_axis.setLine(
diff --git a/b_asic/scheduler_gui/graphics_component_item.py b/b_asic/scheduler_gui/graphics_component_item.py
index ffc9b1a6..4feb0929 100644
--- a/b_asic/scheduler_gui/graphics_component_item.py
+++ b/b_asic/scheduler_gui/graphics_component_item.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
-"""B-ASIC Scheduler-gui Graphics Component Item Module.
+"""
+B-ASIC Scheduler-gui Graphics Component Item Module.
 
 Contains the scheduler-gui GraphicsComponentItem class for drawing and maintain a component in a graph.
 """
@@ -28,7 +29,7 @@ from b_asic.scheduler_gui._preferences import (
 
 
 class GraphicsComponentItem(QGraphicsItemGroup):
-    """A class to represent an component in a graph."""
+    """Class to represent a component in a graph."""
 
     _scale: float = 1.0
     """Static, changed from MainWindow."""
@@ -49,7 +50,8 @@ class GraphicsComponentItem(QGraphicsItemGroup):
         height: float = 0.75,
         parent: Optional[QGraphicsItem] = None,
     ):
-        """Constructs a GraphicsComponentItem. 'parent' is passed to QGraphicsItemGroup's constructor.
+        """
+        Construct a GraphicsComponentItem. *parent* is passed to QGraphicsItemGroup's constructor.
         """
         super().__init__(parent=parent)
         self._operation = operation
@@ -80,7 +82,7 @@ class GraphicsComponentItem(QGraphicsItemGroup):
     #     return True
 
     def clear(self) -> None:
-        """Sets all children's parent to 'None' and delete the axis."""
+        """Sets all children's parent to None and delete the axis."""
         for item in self.childItems():
             item.setParentItem(None)
             del item
@@ -97,8 +99,10 @@ class GraphicsComponentItem(QGraphicsItemGroup):
 
     @property
     def height(self) -> float:
-        """Get or set the current component height. Setting the height to a new
-        value will update the component automatically."""
+        """
+        Get or set the current component height. Setting the height to a new
+        value will update the component automatically.
+        """
         return self._height
 
     @height.setter
@@ -115,7 +119,7 @@ class GraphicsComponentItem(QGraphicsItemGroup):
 
     @property
     def event_items(self) -> List[QGraphicsItem]:
-        """Returnes a list of objects, that receives events."""
+        """Returns a list of objects, that receives events."""
         return [self]
 
     def get_port_location(self, key) -> QPointF:
@@ -146,7 +150,7 @@ class GraphicsComponentItem(QGraphicsItemGroup):
         pen2 = QPen(Qt.black)  # used by port outline
         pen2.setWidthF(0)
         # pen2.setCosmetic(True)
-        port_size = 7 / self._scale  # the diameter of an port
+        port_size = 7 / self._scale  # the diameter of a port
 
         execution_time = QColor(OPERATION_EXECUTION_TIME_INACTIVE)
         execution_time.setAlpha(200)  # 0-255
@@ -154,9 +158,10 @@ class GraphicsComponentItem(QGraphicsItemGroup):
         pen3.setColor(execution_time)
         pen3.setWidthF(3 / self._scale)
 
-        ## component path
+        # component path
         def draw_component_path(keys: List[str], reversed: bool) -> None:
-            """Draws component path and also register port positions in self._ports dictionary.
+            """
+            Draws component path and also register port positions in self._ports dictionary.
             """
             nonlocal x
             nonlocal y
@@ -171,7 +176,7 @@ class GraphicsComponentItem(QGraphicsItemGroup):
                 y = old_y + neg * (self._height / len(keys))
                 component_path.lineTo(x, y)  # vertical line
                 # register the port pos in dictionary
-                port_x = x  # Port coords is at the center
+                port_x = x  # Port coordinates is at the center
                 port_y = (
                     y - neg * abs(y - old_y) / 2
                 )  # of previous vertical line.
@@ -205,12 +210,12 @@ class GraphicsComponentItem(QGraphicsItemGroup):
         draw_component_path(output_keys, True)  # draw output side
         component_path.closeSubpath()
 
-        ## component item
+        # component item
         self._component_item = QGraphicsPathItem(component_path)
         self._component_item.setPen(pen1)
         self._set_background(Qt.lightGray)  # used by component filling
 
-        ## ports item
+        # ports item
         for port_dict in self._ports.values():
             port_pos = self.mapToParent(port_dict["pos"])
             port = QGraphicsEllipseItem(
@@ -221,14 +226,14 @@ class GraphicsComponentItem(QGraphicsItemGroup):
             port.setPos(port_pos.x(), port_pos.y())
             self._port_items.append(port)
 
-        ## op-id/label
+        # op-id/label
         self._label_item = QGraphicsSimpleTextItem(self._operation.graph_id)
         self._label_item.setScale(self._label_item.scale() / self._scale)
         center = self._component_item.boundingRect().center()
         center -= self._label_item.boundingRect().center() / self._scale
         self._label_item.setPos(self._component_item.pos() + center)
 
-        ## execution time
+        # execution time
         if self._operation.execution_time is not None:
             self._execution_time_item = QGraphicsRectItem(
                 0, 0, self._operation.execution_time, self._height
@@ -236,7 +241,7 @@ class GraphicsComponentItem(QGraphicsItemGroup):
             self._execution_time_item.setPen(pen3)
             # self._execution_time_item.setBrush(brush3)
 
-        ## item group, consist of component_item, port_items and execution_time_item
+        # item group, consist of component_item, port_items and execution_time_item
         self.addToGroup(self._component_item)
         for port in self._port_items:
             self.addToGroup(port)
diff --git a/b_asic/scheduler_gui/graphics_graph_event.py b/b_asic/scheduler_gui/graphics_graph_event.py
index dab4c1e9..7af6938a 100644
--- a/b_asic/scheduler_gui/graphics_graph_event.py
+++ b/b_asic/scheduler_gui/graphics_graph_event.py
@@ -196,12 +196,11 @@ class GraphicsGraphEvent:  # PyQt5
 
         # Qt.DragMoveCursor
         # button = event.button()
-        def update_pos(item, delta_x):
-            pos = item.x() + delta_x
+        def update_pos(item, dx):
+            pos = item.x() + dx
             if self.is_component_valid_pos(item, pos):
-                # self.prepareGeometryChange()
                 item.setX(pos)
-                self._current_pos.setX(self._current_pos.x() + delta_x)
+                self._current_pos.setX(self._current_pos.x() + dx)
                 self._redraw_lines(item)
 
         item: GraphicsComponentItem = self.scene().mouseGrabberItem()
@@ -229,6 +228,11 @@ class GraphicsGraphEvent:  # PyQt5
         item: GraphicsComponentItem = self.scene().mouseGrabberItem()
         self.set_item_inactive(item)
         self.set_new_starttime(item)
+        pos = item.x()
+        if pos > self.schedule.schedule_time:
+            pos = pos % self.schedule.schedule_time
+            item.setX(pos)
+            self._redraw_lines(item)
 
     def comp_mouseDoubleClickEvent(
         self, event: QGraphicsSceneMouseEvent
@@ -249,13 +253,13 @@ class GraphicsGraphEvent:  # PyQt5
 
         # Qt.DragMoveCursor
         # button = event.button()
-        def update_pos(item, delta_x):
-            pos = item.x() + delta_x
-            if self.is_valid_delta_time(self._delta_time + delta_x):
+        def update_pos(item, dx):
+            pos = item.x() + dx
+            if self.is_valid_delta_time(self._delta_time + dx):
                 # self.prepareGeometryChange()
                 item.setX(pos)
-                self._current_pos.setX(self._current_pos.x() + delta_x)
-                self._delta_time += delta_x
+                self._current_pos.setX(self._current_pos.x() + dx)
+                self._delta_time += dx
                 item.set_text(self._delta_time)
 
         item: GraphicsTimelineItem = self.scene().mouseGrabberItem()
diff --git a/b_asic/scheduler_gui/graphics_graph_item.py b/b_asic/scheduler_gui/graphics_graph_item.py
index 8a3c43dd..053b67e5 100644
--- a/b_asic/scheduler_gui/graphics_graph_item.py
+++ b/b_asic/scheduler_gui/graphics_graph_item.py
@@ -84,12 +84,12 @@ class GraphicsGraphItem(
             return False
         if (
             self.schedule.cyclic
-            and new_start_time > self.schedule.schedule_time
+            and new_start_time > self.schedule.schedule_time + 1
         ):
             return False
         if (
             not self.schedule.cyclic
-            and new_start_time + end_time > self.schedule.schedule_time
+            and new_start_time + end_time > self.schedule.schedule_time + 1
         ):
             return False
 
@@ -153,7 +153,7 @@ class GraphicsGraphItem(
 
     @property
     def event_items(self) -> List[QGraphicsItem]:
-        """Returnes a list of objects, that receives events."""
+        """Return a list of objects that receives events."""
         return self._event_items
 
     def _make_graph(self) -> None:
diff --git a/b_asic/scheduler_gui/graphics_timeline_item.py b/b_asic/scheduler_gui/graphics_timeline_item.py
index 9566fd71..fe02ce2a 100644
--- a/b_asic/scheduler_gui/graphics_timeline_item.py
+++ b/b_asic/scheduler_gui/graphics_timeline_item.py
@@ -3,7 +3,7 @@
 """
 B-ASIC Scheduler-gui Graphics Timeline Item Module.
 
-Contains the a scheduler-gui GraphicsTimelineItem class for drawing and
+Contains the scheduler-gui GraphicsTimelineItem class for drawing and
 maintain the timeline in a graph.
 """
 from typing import List, Optional, overload
@@ -114,5 +114,5 @@ class GraphicsTimelineItem(QGraphicsLineItem):
 
     @property
     def event_items(self) -> List[QGraphicsItem]:
-        """Returnes a list of objects, that receives events."""
+        """Return a list of objects that receives events."""
         return [self]
diff --git a/b_asic/scheduler_gui/main_window.py b/b_asic/scheduler_gui/main_window.py
index cb3e9a20..5d24b40a 100644
--- a/b_asic/scheduler_gui/main_window.py
+++ b/b_asic/scheduler_gui/main_window.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
-"""B-ASIC Scheduler-gui Module.
+"""
+B-ASIC Scheduler-gui Module.
 
 Contains the scheduler-gui MainWindow class for scheduling operations in an SFG.
 
@@ -55,7 +56,7 @@ log = logger.getLogger()
 sys.excepthook = logger.handle_exceptions
 
 
-# Debug struff
+# Debug stuff
 if __debug__:
     log.setLevel("DEBUG")
 
@@ -79,7 +80,7 @@ if __debug__:
 
 
 # The following QCoreApplication values is used for QSettings among others
-QCoreApplication.setOrganizationName("Linöping University")
+QCoreApplication.setOrganizationName("Linköping University")
 QCoreApplication.setOrganizationDomain("liu.se")
 QCoreApplication.setApplicationName("B-ASIC Scheduler")
 # QCoreApplication.setApplicationVersion(__version__)     # TODO: read from packet __version__
@@ -156,9 +157,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
         """Get the current schedule."""
         return self._schedule
 
-    ###############
-    #### Slots ####
-    ###############
+    #########
+    # Slots #
+    #########
     @Slot()
     def _actionTbtn(self) -> None:
         # TODO: remove
@@ -280,8 +281,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
 
     @Slot()
     def save(self) -> None:
-        """SLOT() for SIGNAL(menu_save.triggered)
-        This method save an schedule."""
+        """
+        SLOT() for SIGNAL(menu_save.triggered)
+        This method save a schedule.
+        """
         # TODO: all
         self._printButtonPressed("save_schedule()")
         self.update_statusbar(self.tr("Schedule saved successfully"))
@@ -289,7 +292,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
     @Slot()
     def save_as(self) -> None:
         """SLOT() for SIGNAL(menu_save_as.triggered)
-        This method save as an schedule."""
+        This method save as a schedule."""
         # TODO: all
         self._printButtonPressed("save_schedule()")
         self.update_statusbar(self.tr("Schedule saved successfully"))
@@ -313,17 +316,21 @@ class MainWindow(QMainWindow, Ui_MainWindow):
 
     @Slot(bool)
     def hide_exit_dialog(self, checked: bool) -> None:
-        """SLOT(bool) for SIGNAL(menu_exit_dialog.triggered)
+        """
+        SLOT(bool) for SIGNAL(menu_exit_dialog.triggered)
         Takes in a boolean and stores 'checked' in 'hide_exit_dialog' item in
-        settings."""
+        settings.
+        """
         s = QSettings()
         s.setValue("mainwindow/hide_exit_dialog", checked)
 
     @Slot(int, int)
     def _splitter_moved(self, pos: int, index: int) -> None:
-        """SLOT(int, int) for SIGNAL(splitter.splitterMoved)
+        """
+        SLOT(int, int) for SIGNAL(splitter.splitterMoved)
         Callback method used to check if the right widget (info window)
-        has collapsed. Update the checkbutton accordingly."""
+        has collapsed. Update the checkbutton accordingly.
+        """
         width = self.splitter.sizes()[1]
 
         if width == 0:
@@ -336,34 +343,42 @@ class MainWindow(QMainWindow, Ui_MainWindow):
 
     @Slot(str)
     def info_table_update_component(self, op_id: str) -> None:
-        """SLOT(str) for SIGNAL(_graph._signals.component_selected)
-        Taked in an operator-id, first clears the 'Operator' part of the info
+        """
+        SLOT(str) for SIGNAL(_graph._signals.component_selected)
+        Takes in an operator-id, first clears the 'Operator' part of the info
         table and then fill in the table with new values from the operator
-        associated with 'op_id'."""
+        associated with 'op_id'.
+        """
         self.info_table_clear_component()
         self._info_table_fill_component(op_id)
 
     @Slot()
     def info_table_update_schedule(self) -> None:
-        """SLOT() for SIGNAL(_graph._signals.schedule_time_changed)
-        Updates the 'Schedule' part of the info table."""
+        """
+        SLOT() for SIGNAL(_graph._signals.schedule_time_changed)
+        Updates the 'Schedule' part of the info table.
+        """
         self.info_table.item(1, 1).setText(str(self.schedule.schedule_time))
 
     @Slot(QRectF)
     def shrink_scene_to_min_size(self, rect: QRectF) -> None:
-        """SLOT(QRectF) for SIGNAL(_scene.sceneRectChanged)
+        """
+        SLOT(QRectF) for SIGNAL(_scene.sceneRectChanged)
         Takes in a QRectF (unused) and shrink the scene bounding rectangle to
-        it's minimum size, when the bounding rectangle signals a change in
-        geometry."""
+        its minimum size, when the bounding rectangle signals a change in
+        geometry.
+        """
         self._scene.setSceneRect(self._scene.itemsBoundingRect())
 
-    ################
-    #### Events ####
-    ################
+    ##########
+    # Events #
+    ##########
     def _close_event(self, event: QCloseEvent) -> None:
-        """EVENT: Replaces QMainWindow default closeEvent(QCloseEvent) event. Takes
+        """
+        EVENT: Replaces QMainWindow default closeEvent(QCloseEvent) event. Takes
         in a QCloseEvent and display an exit dialog, depending on
-        'hide_exit_dialog' in settings."""
+        'hide_exit_dialog' in settings.
+        """
         s = QSettings()
         hide_dialog = s.value("mainwindow/hide_exit_dialog", False, bool)
         ret = QMessageBox.StandardButton.Yes
@@ -396,9 +411,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
         else:
             event.ignore()
 
-    #################################
-    #### Helper member functions ####
-    #################################
+    ###########################
+    # Helper member functions #
+    ###########################
     def _printButtonPressed(self, func_name: str) -> None:
         # TODO: remove
 
@@ -407,7 +422,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
         alert.exec_()
 
     def open(self, schedule: Schedule) -> None:
-        """Takes in an Schedule and creates a GraphicsGraphItem object."""
+        """Take a Schedule and create a GraphicsGraphItem object."""
         self.close_schedule()
         self._schedule = deepcopy(schedule)
         self._graph = GraphicsGraphItem(self.schedule)
@@ -425,7 +440,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
         self.update_statusbar(self.tr("Schedule loaded successfully"))
 
     def update_statusbar(self, msg: str) -> None:
-        """Takes in an str and write 'msg' to the statusbar with temporarily policy.
+        """Take a str and write *msg* to the statusbar with temporarily policy.
         """
         self.statusbar.showMessage(msg)
 
@@ -434,7 +449,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
         s = QSettings()
         s.setValue(
             "mainwindow/maximized", self.isMaximized()
-        )  # window: maximized, in X11 - alwas False
+        )  # window: maximized, in X11 - always False
         s.setValue("mainwindow/pos", self.pos())  # window: pos
         s.setValue("mainwindow/size", self.size())  # window: size
         s.setValue(
@@ -474,25 +489,24 @@ class MainWindow(QMainWindow, Ui_MainWindow):
         log.debug("Settings read from '{}'.".format(s.fileName()))
 
     def info_table_fill_schedule(self, schedule: Schedule) -> None:
-        """Takes in a Schedule and fill in the 'Schedule' part of the info table
-        with values from 'schedule'"""
-        self.info_table.insertRow(1)
+        """
+        Take a Schedule and fill in the 'Schedule' part of the info table
+        with values from *schedule*.
+        """
         self.info_table.insertRow(1)
         self.info_table.insertRow(1)
         self.info_table.setItem(1, 0, QTableWidgetItem("Schedule Time"))
         self.info_table.setItem(2, 0, QTableWidgetItem("Cyclic"))
-        self.info_table.setItem(3, 0, QTableWidgetItem("Resolution"))
         self.info_table.setItem(
             1, 1, QTableWidgetItem(str(schedule.schedule_time))
         )
         self.info_table.setItem(2, 1, QTableWidgetItem(str(schedule.cyclic)))
-        self.info_table.setItem(
-            3, 1, QTableWidgetItem(str(schedule.resolution))
-        )
 
     def _info_table_fill_component(self, op_id: str) -> None:
-        """Taked in an operator-id and fill in the 'Operator' part of the info
-        table with values from the operator associated with 'op_id'."""
+        """
+        Take an operator-id and fill in the 'Operator' part of the info
+        table with values from the operator associated with *op_id*.
+        """
         op: GraphComponent = self.schedule.sfg.find_by_id(op_id)
         si = self.info_table.rowCount()  # si = start index
 
diff --git a/test/test_scheduler_gui.py b/test/test_scheduler_gui.py
index 3a045f85..ebfdad70 100644
--- a/test/test_scheduler_gui.py
+++ b/test/test_scheduler_gui.py
@@ -1,5 +1,7 @@
 import pytest
 
+from b_asic.core_operations import Addition, ConstantMultiplication
+from b_asic.schedule import Schedule
 try:
     import b_asic.scheduler_gui as GUI
 except ImportError:
@@ -11,3 +13,16 @@ def test_start(qtbot):
     qtbot.addWidget(widget)
 
     widget.exit_app()
+
+
+def test_load_schedule(qtbot, sfg_simple_filter):
+    sfg_simple_filter.set_latency_of_type(Addition.type_name(), 5)
+    sfg_simple_filter.set_latency_of_type(
+        ConstantMultiplication.type_name(), 4
+    )
+
+    widget = GUI.MainWindow()
+    qtbot.addWidget(widget)
+    schedule = Schedule(sfg_simple_filter)
+    widget.open(schedule)
+    assert widget.statusbar.currentMessage() == "Schedule loaded successfully"
-- 
GitLab