Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • da/B-ASIC
  • lukja239/B-ASIC
  • robal695/B-ASIC
3 results
Show changes
Commits on Source (28)
Showing
with 1043 additions and 427 deletions
......@@ -23,6 +23,7 @@ The following packages are required in order to build the library:
- [Matplotlib](https://matplotlib.org/)
- [NumPy](https://numpy.org/)
- [QtPy](https://github.com/spyder-ide/qtpy)
- [setuptools_scm](https://github.com/pypa/setuptools_scm/)
- Qt 5 or 6, with Python bindings, one of:
- pyside2
- pyqt5
......@@ -52,6 +53,8 @@ To generate the documentation, the following additional packages are required:
- [Sphinx](https://www.sphinx-doc.org/)
- [Furo](https://pradyunsg.me/furo/)
- [numpydoc](https://numpydoc.readthedocs.io/)
- [Sphinx-Gallery](https://sphinx-gallery.github.io/)
- [SciPy](https://scipy.org/)
### Using CMake directly
......
......@@ -28,7 +28,15 @@ class DragButton(QPushButton):
"""
Drag button class.
This class creates a drag button which can be clicked, dragged and dropped.
This class creates a button which can be clicked, dragged and dropped.
Parameters
----------
name
operation
is_show_name
window
parent
"""
connectionRequested = Signal(QPushButton)
......
"""
B-ASIC port button module.
"""
from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import QMenu, QPushButton
class PortButton(QPushButton):
"""
A button corresponding to a port.
Parameters
----------
name
operation
port
window
parent
"""
connectionRequested = Signal(QPushButton)
moved = Signal()
......
"""
B-ASIC select SFG window.
"""
from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import QComboBox, QDialog, QPushButton, QVBoxLayout
......
"""
B-ASIC window to show precedence graph.
"""
from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import (
QCheckBox,
......
"""
B-ASIC window to simulate an SFG.
"""
from matplotlib.backends.backend_qt5agg import (
FigureCanvasQTAgg as FigureCanvas,
)
......
......@@ -3,3 +3,9 @@ EXECUTION_TIME_COLOR = (255, 100, 66, 200)
SIGNAL_COLOR = (0, 0, 0)
SIGNAL_LINEWIDTH = 1.0
OPERATION_GAP = 0.5
SCHEDULE_OFFSET = 0.2
SPLINE_OFFSET = 0.2
......@@ -294,6 +294,10 @@ class Multiplication(AbstractOperation):
Operation execution time (time units before operator can be
reused).
See also
========
ConstantMultiplication
"""
def __init__(
......@@ -332,6 +336,9 @@ class Division(AbstractOperation):
.. math:: y = \frac{x_0}{x_1}
See also
========
Reciprocal
"""
def __init__(
......@@ -371,6 +378,10 @@ class Min(AbstractOperation):
.. math:: y = \min\{x_0 , x_1\}
.. note:: Only real-valued numbers are supported.
See also
========
Max
"""
def __init__(
......@@ -414,6 +425,10 @@ class Max(AbstractOperation):
.. math:: y = \max\{x_0 , x_1\}
.. note:: Only real-valued numbers are supported.
See also
========
Min
"""
def __init__(
......@@ -614,8 +629,8 @@ class Butterfly(AbstractOperation):
.. math::
\begin{eqnarray}
y_0 = x_0 + x_1\\
y_1 = x_0 - x_1
y_0 & = & x_0 + x_1\\
y_1 & = & x_0 - x_1
\end{eqnarray}
"""
......@@ -692,8 +707,8 @@ class SymmetricTwoportAdaptor(AbstractOperation):
.. math::
\begin{eqnarray}
y_0 = x_1 + \text{value}\times\left(x_1 - x_0\right)\\
y_1 = x_0 + \text{value}\times\left(x_1 - x_0\right)
y_0 & = & x_1 + \text{value}\times\left(x_1 - x_0\right)\\
y_1 & = & x_0 + \text{value}\times\left(x_1 - x_0\right)
\end{eqnarray}
"""
......@@ -707,7 +722,7 @@ class SymmetricTwoportAdaptor(AbstractOperation):
latency_offsets: Optional[Dict[str, int]] = None,
execution_time: Optional[int] = None,
):
"""Construct a Butterfly operation."""
"""Construct a SymmetricTwoportAdaptor operation."""
super().__init__(
input_count=2,
output_count=2,
......@@ -736,3 +751,43 @@ class SymmetricTwoportAdaptor(AbstractOperation):
def value(self, value: Number) -> None:
"""Set the constant value of this operation."""
self.set_param("value", value)
class Reciprocal(AbstractOperation):
r"""
Reciprocal operation.
Gives the reciprocal of its input.
.. math:: y = \frac{1}{x}
See also
========
Division
"""
def __init__(
self,
src0: Optional[SignalSourceProvider] = None,
name: Name = Name(""),
latency: Optional[int] = None,
latency_offsets: Optional[Dict[str, int]] = None,
execution_time: Optional[int] = None,
):
"""Construct an Reciprocal operation."""
super().__init__(
input_count=1,
output_count=1,
name=Name(name),
input_sources=[src0],
latency=latency,
latency_offsets=latency_offsets,
execution_time=execution_time,
)
@classmethod
def type_name(cls) -> TypeName:
return TypeName("rec")
def evaluate(self, a):
return 1 / a
......@@ -40,6 +40,7 @@ if TYPE_CHECKING:
ConstantMultiplication,
Division,
Multiplication,
Reciprocal,
Subtraction,
)
from b_asic.signal_flow_graph import SFG
......@@ -135,7 +136,7 @@ class Operation(GraphComponent, SignalSourceProvider):
@abstractmethod
def __rtruediv__(
self, src: Union[SignalSourceProvider, Number]
) -> "Division":
) -> Union["Division", "Reciprocal"]:
"""
Overloads the division operator to make it return a new Division operation
object that is connected to the self and other objects.
......@@ -240,15 +241,27 @@ class Operation(GraphComponent, SignalSourceProvider):
) -> Number:
"""
Evaluate the output at the given index of this operation with the given input values.
The *results* parameter will be used to store any results (including intermediate results)
for caching.
The *delays* parameter will be used to get the current value of any intermediate delays
that are encountered, and be updated with their new values.
The *prefix* parameter will be used as a prefix for the key string when storing results/delays.
The *bits_override* parameter specifies a word length override when truncating inputs
which ignores the word length specified by the input signal.
The *truncate* parameter specifies whether input truncation should be enabled in the first
place. If set to False, input values will be used directly without any bit truncation.
Parameters
----------
index : int
Which output to return the value for.
input_values : array of float or complex
The input values.
results : MutableResultMap. optional
Used to store any results (including intermediate results)
for caching.
delays : MutableDelayMap. optional
Used to get the current value of any intermediate delay elements
that are encountered, and be updated with their new values.
prefix : str, optional
Used as a prefix for the key string when storing results/delays.
bits_override ; int, optional
Specifies a word length override when truncating inputs
which ignores the word length specified by the input signal.
truncate : bool, default: True
Specifies whether input truncation should be enabled in the first
place. If set to False, input values will be used directly without any bit truncation.
See also
========
......@@ -263,7 +276,10 @@ class Operation(GraphComponent, SignalSourceProvider):
) -> Sequence[Optional[Number]]:
"""
Get all current outputs of this operation, if available.
See current_output for more information.
See also
========
current_output
"""
raise NotImplementedError
......@@ -372,7 +388,7 @@ class Operation(GraphComponent, SignalSourceProvider):
self,
) -> Tuple[List[List[float]], List[List[float]]]:
"""
Get a tuple constaining coordinates for the two polygons outlining
Return a tuple containing coordinates for the two polygons outlining
the latency and execution time of the operation.
The polygons are corresponding to a start time of 0 and are of height 1.
"""
......@@ -383,7 +399,7 @@ class Operation(GraphComponent, SignalSourceProvider):
self,
) -> Tuple[List[List[float]], List[List[float]]]:
"""
Get a tuple constaining coordinates for inputs and outputs, respectively.
Return a tuple containing coordinates for inputs and outputs, respectively.
These maps to the polygons and are corresponding to a start time of 0
and height 1.
"""
......@@ -468,7 +484,11 @@ class AbstractOperation(Operation, AbstractGraphComponent):
)
for i, src in enumerate(input_sources):
if src is not None:
self._input_ports[i].connect(src.source)
if isinstance(src, Signal):
# Already existing signal
src.set_destination(self._input_ports[i])
else:
self._input_ports[i].connect(src.source)
# Set specific latency_offsets
if latency_offsets is not None:
......@@ -481,9 +501,9 @@ class AbstractOperation(Operation, AbstractGraphComponent):
for inp in self.inputs:
if inp.latency_offset is None:
inp.latency_offset = 0
for outp in self.outputs:
if outp.latency_offset is None:
outp.latency_offset = latency
for output in self.outputs:
if output.latency_offset is None:
output.latency_offset = latency
self._execution_time = execution_time
......@@ -573,13 +593,16 @@ class AbstractOperation(Operation, AbstractGraphComponent):
def __rtruediv__(
self, src: Union[SignalSourceProvider, Number]
) -> "Division":
) -> Union["Division", "Reciprocal"]:
# Import here to avoid circular imports.
from b_asic.core_operations import Constant, Division
from b_asic.core_operations import Constant, Division, Reciprocal
return Division(
Constant(src) if isinstance(src, Number) else src, self
)
if isinstance(src, Number):
if src == 1:
return Reciprocal(self)
else:
return Division(Constant(src), self)
return Division(src, self)
def __lshift__(self, src: SignalSourceProvider) -> Signal:
if self.input_count != 1:
......@@ -816,10 +839,10 @@ class AbstractOperation(Operation, AbstractGraphComponent):
new_component: Operation = cast(
Operation, super().copy_component(*args, **kwargs)
)
for i, inp in enumerate(self.inputs):
new_component.input(i).latency_offset = inp.latency_offset
for i, outp in enumerate(self.outputs):
new_component.output(i).latency_offset = outp.latency_offset
for i, input in enumerate(self.inputs):
new_component.input(i).latency_offset = input.latency_offset
for i, output in enumerate(self.outputs):
new_component.output(i).latency_offset = output.latency_offset
new_component.execution_time = self._execution_time
return new_component
......@@ -911,7 +934,7 @@ class AbstractOperation(Operation, AbstractGraphComponent):
@property
def latency(self) -> int:
if None in [inp.latency_offset for inp in self.inputs] or None in [
outp.latency_offset for outp in self.outputs
output.latency_offset for output in self.outputs
]:
raise ValueError(
"All native offsets have to set to a non-negative value to"
......@@ -921,10 +944,10 @@ class AbstractOperation(Operation, AbstractGraphComponent):
return max(
(
(
cast(int, outp.latency_offset)
- cast(int, inp.latency_offset)
cast(int, output.latency_offset)
- cast(int, input.latency_offset)
)
for outp, inp in it.product(self.outputs, self.inputs)
for output, input in it.product(self.outputs, self.inputs)
)
)
......@@ -932,11 +955,11 @@ class AbstractOperation(Operation, AbstractGraphComponent):
def latency_offsets(self) -> Dict[str, Optional[int]]:
latency_offsets = {}
for i, inp in enumerate(self.inputs):
latency_offsets[f"in{i}"] = inp.latency_offset
for i, input in enumerate(self.inputs):
latency_offsets[f"in{i}"] = input.latency_offset
for i, outp in enumerate(self.outputs):
latency_offsets[f"out{i}"] = outp.latency_offset
for i, output in enumerate(self.outputs):
latency_offsets[f"out{i}"] = output.latency_offset
return latency_offsets
......@@ -953,17 +976,19 @@ class AbstractOperation(Operation, AbstractGraphComponent):
port_str = port_str.lower()
if port_str.startswith("in"):
index_str = port_str[2:]
assert index_str.isdigit(), (
"Incorrectly formatted index in string, expected 'in' +"
f" index, got: {port_str!r}"
)
if not index_str.isdigit():
raise ValueError(
"Incorrectly formatted index in string, expected 'in'"
f" + index, got: {port_str!r}"
)
self.input(int(index_str)).latency_offset = latency_offset
elif port_str.startswith("out"):
index_str = port_str[3:]
assert index_str.isdigit(), (
"Incorrectly formatted index in string, expected 'out' +"
f" index, got: {port_str!r}"
)
if not index_str.isdigit():
raise ValueError(
"Incorrectly formatted index in string, expected"
f" 'out' + index, got: {port_str!r}"
)
self.output(int(index_str)).latency_offset = latency_offset
else:
raise ValueError(
......@@ -1051,20 +1076,20 @@ class AbstractOperation(Operation, AbstractGraphComponent):
def get_io_coordinates(
self,
) -> Tuple[List[List[float]], List[List[float]]]:
self._check_all_latencies_set()
# Doc-string inherited
input_coords = [
self._check_all_latencies_set()
input_coordinates = [
[
self.inputs[k].latency_offset,
(1 + 2 * k) / (2 * len(self.inputs)),
]
for k in range(len(self.inputs))
]
output_coords = [
output_coordinates = [
[
self.outputs[k].latency_offset,
(1 + 2 * k) / (2 * len(self.outputs)),
]
for k in range(len(self.outputs))
]
return input_coords, output_coords
return input_coordinates, output_coordinates
......@@ -209,6 +209,9 @@ class InputPort(AbstractPort):
"""
if self._source_signal is not None:
raise ValueError("Cannot connect already connected input port.")
if isinstance(src, Signal):
src.set_destination(self)
return src
# self._source_signal is set by the signal constructor.
return Signal(source=src.source, destination=self, name=Name(name))
......
This diff is collapsed.
......@@ -5,7 +5,6 @@ Graphical user interface for B-ASIC scheduler.
__author__ = "Andreas Bolin"
# __all__ = ['main_window', 'graphics_graph', 'component_item', 'graphics_axes', 'graphics_timeline_item']
from b_asic.scheduler_gui._version import *
from b_asic.scheduler_gui.axes_item import *
from b_asic.scheduler_gui.logger import *
from b_asic.scheduler_gui.main_window import *
......
......@@ -14,3 +14,10 @@ OPERATION_LATENCY_INACTIVE = QColor(*LATENCY_COLOR)
OPERATION_LATENCY_ACTIVE = QColor(0, 207, 181)
OPERATION_EXECUTION_TIME_INACTIVE = QColor(*EXECUTION_TIME_COLOR)
OPERATION_EXECUTION_TIME_ACTIVE = QColor(*EXECUTION_TIME_COLOR)
OPERATION_HEIGHT = 0.75
OPERATION_GAP = (
1 - OPERATION_HEIGHT
) # TODO: For now, should really fix the bug
SCHEDULE_INDENT = 0.2
__version_info__ = ("0", "1")
__version__ = ".".join(__version_info__)
......@@ -20,11 +20,24 @@ from qtpy.QtWidgets import (
)
# B-ASIC
from b_asic.scheduler_gui._preferences import SCHEDULE_INDENT
from b_asic.scheduler_gui.timeline_item import TimelineItem
class AxesItem(QGraphicsItemGroup):
"""A class to represent axes in a graph."""
"""
A class to represent axes in a graph.
Parameters
----------
width
height
width_indent
height_indent
width_padding
height_padding
parent
"""
_scale: float = 1.0
"""Static, changed from MainWindow."""
......@@ -51,14 +64,14 @@ class AxesItem(QGraphicsItemGroup):
self,
width: int,
height: int,
width_indent: float = 0.2,
height_indent: float = 0.2,
width_indent: float = SCHEDULE_INDENT,
height_indent: float = SCHEDULE_INDENT,
width_padding: float = 0.6,
height_padding: float = 0.5,
parent: Optional[QGraphicsItem] = None,
):
"""
Constructs a AxesItem.
Class for an AxesItem.
*parent* is passed to QGraphicsItemGroup's constructor.
"""
super().__init__(parent=parent)
......@@ -129,10 +142,10 @@ class AxesItem(QGraphicsItemGroup):
"""
return self._height
# @height.setter
# def height(self, height: int) -> None:
# if self._height != height:
# self.update_axes(height = height)
@height.setter
def height(self, height: int) -> None:
if self._height != height:
self.update_axes(height=height)
# @property
# def width_indent(self) -> float:
......@@ -154,12 +167,13 @@ class AxesItem(QGraphicsItemGroup):
self._event_items.append(item)
def set_height(self, height: int) -> "AxesItem":
# TODO: implement, docstring
# TODO: docstring
if height < 0:
raise ValueError(
f"'height' greater or equal to 0 expected, got: {height}."
)
raise NotImplementedError
self._height = height
self._update_yaxis()
def set_width(self, width: int) -> "AxesItem":
# TODO: docstring
......@@ -275,6 +289,20 @@ class AxesItem(QGraphicsItemGroup):
self._x_scale_labels[index + 1].setText(str(index + 1))
self._x_scale[index + 1].setX(self._x_scale[index + 1].x() + 1)
def _update_yaxis(self) -> None:
self._y_axis.setLine(
0,
0,
0,
-(
self._height_indent
+ self._height
+ self._height_padding
+ 0.05
),
)
self._y_axis.setPen(self._base_pen)
def _make_base(self) -> None:
# x axis
self._x_axis.setLine(0, 0, self._width_indent + self._width_padding, 0)
......@@ -322,16 +350,5 @@ class AxesItem(QGraphicsItemGroup):
) # move timeline
# y-axis
self._y_axis.setLine(
0,
0,
0,
-(
self._height_indent
+ self._height
+ self._height_padding
+ 0.05
),
)
self._y_axis.setPen(self._base_pen)
self._update_yaxis()
self.addToGroup(self._y_axis)
......@@ -10,6 +10,7 @@ Start main-window with start_gui().
import inspect
import os
import sys
import webbrowser
from copy import deepcopy
from importlib.machinery import SourceFileLoader
from typing import Optional, Union, cast
......@@ -43,6 +44,7 @@ from qtpy.QtWidgets import (
# B-ASIC
import b_asic.scheduler_gui.logger as logger
from b_asic._version import __version__
from b_asic.graph_component import GraphComponent, GraphID
from b_asic.schedule import Schedule
from b_asic.scheduler_gui.axes_item import AxesItem
......@@ -65,9 +67,9 @@ if __debug__:
from qtpy import QtCore
QT_API = os.environ.get("QT_API", "")
log.debug("Qt version (runtime): {}".format(QtCore.qVersion()))
log.debug("Qt version (compiletime): {}".format(QtCore.__version__))
log.debug("QT_API: {}".format(QT_API))
log.debug("Qt version (runtime): {}".format(QtCore.qVersion()))
log.debug("Qt version (compile time): {}".format(QtCore.__version__))
log.debug("QT_API: {}".format(QT_API))
if QT_API.lower().startswith("pyside"):
import PySide2
......@@ -83,7 +85,9 @@ if __debug__:
QCoreApplication.setOrganizationName("Linköping University")
QCoreApplication.setOrganizationDomain("liu.se")
QCoreApplication.setApplicationName("B-ASIC Scheduler")
# QCoreApplication.setApplicationVersion(__version__) # TODO: read from packet __version__
QCoreApplication.setApplicationVersion(
__version__
) # TODO: read from packet __version__
class MainWindow(QMainWindow, Ui_MainWindow):
......@@ -93,9 +97,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
_schedule: Union[Schedule, None]
_graph: Union[SchedulerItem, None]
_scale: float
_debug_rects: QGraphicsItemGroup
_debug_rectangles: QGraphicsItemGroup
_splitter_pos: int
_splitter_min: int
_zoom: float
def __init__(self):
"""Initialize Scheduler-gui."""
......@@ -103,7 +108,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self._schedule = None
self._graph = None
self._scale = 75.0
self._debug_rects = None
self._debug_rectangles = None
self._zoom = 1.0
self.setupUi(self)
self._read_settings()
......@@ -123,8 +129,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.menu_quit.triggered.connect(self.close)
self.menu_node_info.triggered.connect(self.show_info_table)
self.menu_exit_dialog.triggered.connect(self.hide_exit_dialog)
self.actionReorder.triggered.connect(self._actionReorder)
self.actionT.triggered.connect(self._actionTbtn)
self.splitter.splitterMoved.connect(self._splitter_moved)
self.actionDocumentation.triggered.connect(self._open_documentation)
# Setup event member functions
self.closeEvent = self._close_event
......@@ -154,7 +162,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
@property
def schedule(self) -> Optional[Schedule]:
"""Get the current schedule."""
"""The current schedule."""
return self._schedule
#########
......@@ -165,19 +173,42 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# TODO: remove
if self.schedule is None:
return
self.schedule.plot_schedule()
self.schedule.plot()
if self._graph is not None:
print(f"filtersChildEvents(): {self._graph.filtersChildEvents()}")
# self._printButtonPressed('callback_pushButton()')
# self._print_button_pressed('callback_pushButton()')
@Slot()
def _open_documentation(self) -> None:
webbrowser.open_new_tab("https://da.gitlab-pages.liu.se/B-ASIC/")
@Slot()
def _actionReorder(self) -> None:
"""Callback to reorder all operations vertically based on start time.
"""
if self.schedule is None:
return
if self._graph is not None:
self._graph._redraw_from_start()
def wheelEvent(self, event) -> None:
"""Zoom in or out using mouse wheel if control is pressed."""
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
old_zoom = self._zoom
self._zoom += event.angleDelta().y() / 2500
self.view.scale(self._zoom, self._zoom)
self._zoom = old_zoom
@Slot()
def _load_schedule_from_pyfile(self) -> None:
"""SLOT() for SIGNAL(menu_load_from_file.triggered)
"""
SLOT() for SIGNAL(menu_load_from_file.triggered)
Load a python script as a module and search for a Schedule object. If
found, opens it."""
found, opens it.
"""
settings = QSettings()
last_file = settings.value(
"mainwindow/last_opened_file",
"scheduler/last_opened_file",
QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0],
str,
)
......@@ -230,42 +261,48 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self,
self.tr("File not found"),
self.tr(
"Could not find any Schedule object in file '{}'."
"Cannot find any Schedule object in file '{}'."
).format(os.path.basename(abs_path_filename)),
)
log.info(
"Could not find any Schedule object in file '{}'.".format(
"Cannot find any Schedule object in file '{}'.".format(
os.path.basename(abs_path_filename)
)
)
del module
return
ret_tuple = QInputDialog.getItem(
self,
self.tr("Load object"),
self.tr(
"Found the following Schedule object(s) in file.\n\n"
"Select an object to proceed:"
),
schedule_obj_list.keys(),
0,
False,
)
if len(schedule_obj_list) == 1:
schedule = [val for val in schedule_obj_list.values()][0]
else:
ret_tuple = QInputDialog.getItem(
self,
self.tr("Load object"),
self.tr(
"Found the following Schedule objects in file.\n\n"
"Select an object to proceed:"
),
schedule_obj_list.keys(),
0,
False,
)
if not ret_tuple[1]: # User canceled the operation
log.debug("Load schedule operation: user canceled")
del module
return
if not ret_tuple[1]: # User canceled the operation
log.debug("Load schedule operation: user canceled")
del module
return
schedule = schedule_obj_list[ret_tuple[0]]
self.open(schedule_obj_list[ret_tuple[0]])
self.open(schedule)
del module
settings.setValue("mainwindow/last_opened_file", abs_path_filename)
settings.setValue("scheduler/last_opened_file", abs_path_filename)
@Slot()
def close_schedule(self) -> None:
"""SLOT() for SIGNAL(menu_close_schedule.triggered)
Closes current schedule."""
"""
SLOT() for SIGNAL(menu_close_schedule.triggered)
Closes current schedule.
"""
if self._graph:
self._graph._signals.component_selected.disconnect(
self.info_table_update_component
......@@ -289,22 +326,26 @@ class MainWindow(QMainWindow, Ui_MainWindow):
This method save a schedule.
"""
# TODO: all
self._printButtonPressed("save_schedule()")
self._print_button_pressed("save_schedule()")
self.update_statusbar(self.tr("Schedule saved successfully"))
@Slot()
def save_as(self) -> None:
"""SLOT() for SIGNAL(menu_save_as.triggered)
This method save as a schedule."""
"""
SLOT() for SIGNAL(menu_save_as.triggered)
This method save as a schedule.
"""
# TODO: all
self._printButtonPressed("save_schedule()")
self._print_button_pressed("save_schedule()")
self.update_statusbar(self.tr("Schedule saved successfully"))
@Slot(bool)
def show_info_table(self, checked: bool) -> None:
"""SLOT(bool) for SIGNAL(menu_node_info.triggered)
"""
SLOT(bool) for SIGNAL(menu_node_info.triggered)
Takes in a boolean and hide or show the info table accordingly with
'checked'."""
'checked'.
"""
# Note: splitter handler index 0 is a hidden splitter handle far most left, use index 1
# settings = QSettings()
_, max_ = self.splitter.getRange(1) # tuple(min, max)
......@@ -324,8 +365,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
Takes in a boolean and stores 'checked' in 'hide_exit_dialog' item in
settings.
"""
s = QSettings()
s.setValue("mainwindow/hide_exit_dialog", checked)
settings = QSettings()
settings.setValue("scheduler/hide_exit_dialog", checked)
@Slot(int, int)
def _splitter_moved(self, pos: int, index: int) -> None:
......@@ -345,15 +386,15 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self._splitter_pos = width
@Slot(str)
def info_table_update_component(self, op_id: GraphID) -> None:
def info_table_update_component(self, graph_id: GraphID) -> None:
"""
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 *graph_id*.
"""
self.info_table_clear_component()
self._info_table_fill_component(op_id)
self._info_table_fill_component(graph_id)
@Slot()
def info_table_update_schedule(self) -> None:
......@@ -385,8 +426,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
in a QCloseEvent and display an exit dialog, depending on
'hide_exit_dialog' in settings.
"""
s = QSettings()
hide_dialog = s.value("mainwindow/hide_exit_dialog", False, bool)
settings = QSettings()
hide_dialog = settings.value("scheduler/hide_exit_dialog", False, bool)
ret = QMessageBox.StandardButton.Yes
if not hide_dialog:
......@@ -410,7 +451,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if ret == QMessageBox.StandardButton.Yes:
if not hide_dialog:
s.setValue("mainwindow/hide_exit_dialog", checkbox.isChecked())
settings.setValue(
"scheduler/hide_exit_dialog", checkbox.isChecked()
)
self._write_settings()
log.info("Exit: {}".format(os.path.basename(__file__)))
event.accept()
......@@ -420,7 +463,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
###########################
# Helper member functions #
###########################
def _printButtonPressed(self, func_name: str) -> None:
def _print_button_pressed(self, func_name: str) -> None:
# TODO: remove
alert = QMessageBox(self)
......@@ -439,6 +482,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self._graph._signals.component_selected.connect(
self.info_table_update_component
)
self._graph._signals.component_moved.connect(
self.info_table_update_component
)
self._graph._signals.schedule_time_changed.connect(
self.info_table_update_schedule
)
......@@ -446,53 +492,63 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.update_statusbar(self.tr("Schedule loaded successfully"))
def update_statusbar(self, msg: str) -> None:
"""Take a str and write *msg* to the statusbar with temporarily policy.
"""
Write *msg* to the statusbar with temporarily policy.
Parameters
----------
msg : str
The message to write.
"""
self.statusbar.showMessage(msg)
def _write_settings(self) -> None:
"""Write settings from MainWindow to Settings."""
s = QSettings()
s.setValue(
"mainwindow/maximized", self.isMaximized()
settings = QSettings()
settings.setValue(
"scheduler/maximized", self.isMaximized()
) # window: maximized, in X11 - always False
s.setValue("mainwindow/pos", self.pos()) # window: pos
s.setValue("mainwindow/size", self.size()) # window: size
s.setValue(
"mainwindow/state", self.saveState()
settings.setValue("scheduler/pos", self.pos()) # window: pos
settings.setValue("scheduler/size", self.size()) # window: size
settings.setValue(
"scheduler/state", self.saveState()
) # toolbars, dockwidgets: pos, size
s.setValue(
"mainwindow/menu/node_info", self.menu_node_info.isChecked()
settings.setValue(
"scheduler/menu/node_info", self.menu_node_info.isChecked()
)
settings.setValue(
"scheduler/splitter/state", self.splitter.saveState()
)
s.setValue("mainwindow/splitter/state", self.splitter.saveState())
s.setValue("mainwindow/splitter/pos", self.splitter.sizes()[1])
settings.setValue("scheduler/splitter/pos", self.splitter.sizes()[1])
if s.isWritable():
log.debug("Settings written to '{}'.".format(s.fileName()))
if settings.isWritable():
log.debug("Settings written to '{}'.".format(settings.fileName()))
else:
log.warning("Settings cant be saved to file, read-only.")
def _read_settings(self) -> None:
"""Read settings from Settings to MainWindow."""
s = QSettings()
if s.value("mainwindow/maximized", defaultValue=False, type=bool):
settings = QSettings()
if settings.value(
"scheduler/maximized", defaultValue=False, type=bool
):
self.showMaximized()
else:
self.move(s.value("mainwindow/pos", self.pos()))
self.resize(s.value("mainwindow/size", self.size()))
self.restoreState(s.value("mainwindow/state", QByteArray()))
self.move(settings.value("scheduler/pos", self.pos()))
self.resize(settings.value("scheduler/size", self.size()))
self.restoreState(settings.value("scheduler/state", QByteArray()))
self.menu_node_info.setChecked(
s.value("mainwindow/menu/node_info", True, bool)
settings.value("scheduler/menu/node_info", True, bool)
)
self.splitter.restoreState(
s.value("mainwindow/splitter/state", QByteArray())
settings.value("scheduler/splitter/state", QByteArray())
)
self._splitter_pos = s.value("mainwindow/splitter/pos", 200, int)
self._splitter_pos = settings.value("scheduler/splitter/pos", 200, int)
self.menu_exit_dialog.setChecked(
s.value("mainwindow/hide_exit_dialog", False, bool)
settings.value("scheduler/hide_exit_dialog", False, bool)
)
log.debug("Settings read from '{}'.".format(s.fileName()))
log.debug("Settings read from '{}'.".format(settings.fileName()))
def info_table_fill_schedule(self, schedule: Schedule) -> None:
"""
......@@ -508,15 +564,15 @@ class MainWindow(QMainWindow, Ui_MainWindow):
)
self.info_table.setItem(2, 1, QTableWidgetItem(str(schedule.cyclic)))
def _info_table_fill_component(self, op_id: GraphID) -> None:
def _info_table_fill_component(self, graph_id: GraphID) -> None:
"""
Take an operator-id and fill in the 'Operator' part of the info
table with values from the operator associated with *op_id*.
table with values from the operator associated with *graph_id*.
"""
if self.schedule is None:
return
op: GraphComponent = cast(
GraphComponent, self.schedule.sfg.find_by_id(op_id)
GraphComponent, self.schedule.sfg.find_by_id(graph_id)
)
si = self.info_table.rowCount() # si = start index
......@@ -537,6 +593,22 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.info_table.setItem(si, 1, QTableWidgetItem(str(value)))
si += 1
self.info_table.insertRow(si)
self.info_table.setItem(si, 0, QTableWidgetItem("Forward slack"))
self.info_table.setItem(
si, 1, QTableWidgetItem(str(self.schedule.forward_slack(graph_id)))
)
si += 1
self.info_table.insertRow(si)
self.info_table.setItem(si, 0, QTableWidgetItem("Backward slack"))
self.info_table.setItem(
si,
1,
QTableWidgetItem(str(self.schedule.backward_slack(graph_id))),
)
si += 1
def info_table_clear(self) -> None:
"""Clears the info table."""
self.info_table_clear_component()
......@@ -555,7 +627,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
"'Operator' not found in info table. It may have been renamed."
)
def exit_app(self):
def exit_app(self) -> None:
"""Exit application."""
log.info("Exiting the application.")
QApplication.quit()
......@@ -573,7 +645,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
)
def start_gui():
def start_gui() -> None:
app = QApplication(sys.argv)
window = MainWindow()
window.show()
......
......@@ -18,7 +18,7 @@
</sizepolicy>
</property>
<property name="windowIcon">
<iconset resource="icons/basic.qrc">
<iconset>
<normaloff>:/icons/basic/small_logo.png</normaloff>:/icons/basic/small_logo.png</iconset>
</property>
<widget class="QWidget" name="centralwidget">
......@@ -108,6 +108,12 @@
<property name="text">
<string>Property</string>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="textAlignment">
<set>AlignLeading|AlignVCenter</set>
</property>
......@@ -126,7 +132,9 @@
</property>
<property name="font">
<font>
<bold>true</bold>
<weight>50</weight>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="background">
......@@ -157,7 +165,8 @@
</property>
<property name="font">
<font>
<bold>true</bold>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="background">
......@@ -224,10 +233,19 @@
</property>
<addaction name="menu_exit_dialog"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>&amp;Help</string>
</property>
<addaction name="actionDocumentation"/>
<addaction name="separator"/>
<addaction name="actionAbout"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menu_Edit"/>
<addaction name="menuView"/>
<addaction name="menuWindow"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
......@@ -246,6 +264,7 @@
<addaction name="separator"/>
<addaction name="menu_node_info"/>
<addaction name="actionT"/>
<addaction name="actionReorder"/>
</widget>
<action name="menu_load_from_file">
<property name="icon">
......@@ -291,7 +310,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="icons/misc.qrc">
<iconset>
<normaloff>:/icons/misc/right_panel.svg</normaloff>
<normalon>:/icons/misc/right_filled_panel.svg</normalon>:/icons/misc/right_panel.svg</iconset>
</property>
......@@ -364,6 +383,25 @@
<string>&amp;Close Schedule</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>About</string>
</property>
</action>
<action name="actionDocumentation">
<property name="text">
<string>Documentation</string>
</property>
</action>
<action name="actionReorder">
<property name="text">
<string>Reorder</string>
</property>
<property name="toolTip">
<string>Reorder schedule based on start time</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
......@@ -30,7 +30,15 @@ from b_asic.scheduler_gui._preferences import (
class OperationItem(QGraphicsItemGroup):
"""Class to represent a component in a graph."""
"""
Class to represent an operation in a graph.
Parameters
----------
operation : Operation
height : float, default: 1.0
parent : QGraphicsItem, optional
"""
_scale: float = 1.0
"""Static, changed from MainWindow."""
......@@ -48,7 +56,7 @@ class OperationItem(QGraphicsItemGroup):
def __init__(
self,
operation: Operation,
height: float = 0.75,
height: float = 1.0,
parent: Optional[QGraphicsItem] = None,
):
"""
......@@ -90,13 +98,13 @@ class OperationItem(QGraphicsItemGroup):
del item
@property
def op_id(self) -> GraphID:
"""Get the op-id."""
def graph_id(self) -> GraphID:
"""The graph-id of the operation that the item corresponds to."""
return self._operation.graph_id
@property
def operation(self) -> Operation:
"""Get the operation."""
"""The operation that the item corresponds to."""
return self._operation
@property
......@@ -116,26 +124,40 @@ class OperationItem(QGraphicsItemGroup):
@property
def end_time(self) -> int:
"""Get the relative end time."""
"""The relative end time."""
return self._end_time
@property
def event_items(self) -> List[QGraphicsItem]:
"""Returns a list of objects, that receives events."""
"""List of objects that receives events."""
return [self]
def get_port_location(self, key) -> QPointF:
def get_port_location(self, key: str) -> QPointF:
"""
Return the location specified by *key*.
Parameters
----------
key : str
The port key.
Returns
-------
The location as a QPointF.
"""
return self.mapToParent(self._ports[key]["pos"])
def set_active(self):
def set_active(self) -> None:
"""Set the item as active, i.e., draw it in special colors."""
self._set_background(OPERATION_LATENCY_ACTIVE)
self.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
def set_inactive(self):
def set_inactive(self) -> None:
"""Set the item as inactive, i.e., draw it in standard colors."""
self._set_background(OPERATION_LATENCY_INACTIVE)
self.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
def _set_background(self, color: QColor):
def _set_background(self, color: QColor) -> None:
brush = QBrush(color)
self._latency_item.setBrush(brush)
......@@ -198,13 +220,13 @@ class OperationItem(QGraphicsItemGroup):
key = f"{prefix}{i}"
self._ports[key]["pos"] = pos
port_pos = self.mapToParent(pos)
port = QGraphicsEllipseItem(
new_port = QGraphicsEllipseItem(
-port_size / 2, -port_size / 2, port_size, port_size
)
port.setPen(port_outline_pen)
port.setBrush(port_filling_brush)
port.setPos(port_pos.x(), port_pos.y())
self._port_items.append(port)
new_port.setPen(port_outline_pen)
new_port.setBrush(port_filling_brush)
new_port.setPos(port_pos.x(), port_pos.y())
self._port_items.append(new_port)
create_ports(inputs, "in")
create_ports(outputs, "out")
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""B-ASIC Scheduler-gui Graphics Graph Event Module.
"""
B-ASIC Scheduler-gui Graphics Graph Event Module.
Contains the scheduler-gui SchedulerEvent class containing event filters and handlers for SchedulerItem objects.
"""
......@@ -20,19 +21,28 @@ from qtpy.QtWidgets import (
)
from b_asic.schedule import Schedule
from b_asic.scheduler_gui._preferences import OPERATION_GAP, OPERATION_HEIGHT
from b_asic.scheduler_gui.axes_item import AxesItem
from b_asic.scheduler_gui.operation_item import OperationItem
from b_asic.scheduler_gui.timeline_item import TimelineItem
class SchedulerEvent: # PyQt5
"""Event filter and handlers for SchedulerItem"""
"""
Event filter and handlers for SchedulerItem.
Parameters
----------
parent : QGraphicsItem, optional
The parent QGraphicsItem.
"""
class Signals(QObject): # PyQt5
"""A class representing signals."""
component_selected = Signal(str)
schedule_time_changed = Signal()
component_moved = Signal(str)
_axes: Optional[AxesItem]
_current_pos: QPointF
......@@ -59,9 +69,9 @@ class SchedulerEvent: # PyQt5
def set_item_inactive(self, item: OperationItem) -> None:
raise NotImplementedError
#################
#### Filters ####
#################
###########
# Filters #
###########
@overload
def installSceneEventFilters(self, filterItems: QGraphicsItem) -> None:
...
......@@ -93,8 +103,10 @@ class SchedulerEvent: # PyQt5
...
def removeSceneEventFilters(self, filterItems) -> None:
"""Removes an event filter on 'filterItems' from 'self'. 'filterItems' can
be one object or a list of objects."""
"""
Removes an event filter on *filterItems* from *self*. *filterItems* can
be one object or a list of objects.
"""
item: OperationItem
for item in filterItems:
item.removeSceneEventFilter(self)
......@@ -105,24 +117,23 @@ class SchedulerEvent: # PyQt5
If False is returned, the event is forwarded to the appropriate child in
the event chain.
"""
handler = None
if isinstance(item, OperationItem): # one component
switch = {
# QEvent.FocusIn: self.comp_focusInEvent,
# QEvent.GraphicsSceneContextMenu: self.comp_contextMenuEvent,
# QEvent.GraphicsSceneDragEnter: self.comp_dragEnterEvent,
# QEvent.GraphicsSceneDragMove: self.comp_dragMoveEvent,
# QEvent.GraphicsSceneDragLeave: self.comp_dragLeaveEvent,
# QEvent.GraphicsSceneDrop: self.comp_dropEvent,
# QEvent.GraphicsSceneHoverEnter: self.comp_hoverEnterEvent,
# QEvent.GraphicsSceneHoverMove: self.comp_hoverMoveEvent,
# QEvent.GraphicsSceneHoverLeave: self.comp_hoverLeaveEvent,
QEvent.GraphicsSceneMouseMove: self.comp_mouseMoveEvent,
QEvent.GraphicsSceneMousePress: self.comp_mousePressEvent,
QEvent.GraphicsSceneMouseRelease: self.comp_mouseReleaseEvent,
# QEvent.GraphicsSceneMouseDoubleClick: self.comp_mouseDoubleClickEvent,
# QEvent.GraphicsSceneWheel: self.comp_wheelEvent
# QEvent.FocusIn: self.operation_focusInEvent,
# QEvent.GraphicsSceneContextMenu: self.operation_contextMenuEvent,
# QEvent.GraphicsSceneDragEnter: self.operation_dragEnterEvent,
# QEvent.GraphicsSceneDragMove: self.operation_dragMoveEvent,
# QEvent.GraphicsSceneDragLeave: self.operation_dragLeaveEvent,
# QEvent.GraphicsSceneDrop: self.operation_dropEvent,
# QEvent.GraphicsSceneHoverEnter: self.operation_hoverEnterEvent,
# QEvent.GraphicsSceneHoverMove: self.operation_hoverMoveEvent,
# QEvent.GraphicsSceneHoverLeave: self.operation_hoverLeaveEvent,
QEvent.GraphicsSceneMouseMove: self.operation_mouseMoveEvent,
QEvent.GraphicsSceneMousePress: self.operation_mousePressEvent,
QEvent.GraphicsSceneMouseRelease: self.operation_mouseReleaseEvent,
# QEvent.GraphicsSceneMouseDoubleClick: self.operation_mouseDoubleClickEvent,
# QEvent.GraphicsSceneWheel: self.operation_wheelEvent
}
handler = switch.get(event.type())
......@@ -145,109 +156,134 @@ class SchedulerEvent: # PyQt5
if handler is not None:
handler(event)
return True
return False # returns False if event is ignored and pass through event to its child
# def sceneEvent(self, event: QEvent) -> bool:
# print(f'sceneEvent() --> {event.type()}')
# # event.accept()
# # QApplication.sendEvent(self.scene(), event)
# return False
###############################################
#### Event Handlers: OperationItem ####
###############################################
def comp_focusInEvent(self, event: QFocusEvent) -> None:
return False
#################################
# Event Handlers: OperationItem #
#################################
def operation_focusInEvent(self, event: QFocusEvent) -> None:
...
def comp_contextMenuEvent(
def operation_contextMenuEvent(
self, event: QGraphicsSceneContextMenuEvent
) -> None:
...
def comp_dragEnterEvent(self, event: QGraphicsSceneDragDropEvent) -> None:
def operation_dragEnterEvent(
self, event: QGraphicsSceneDragDropEvent
) -> None:
...
def comp_dragMoveEvent(self, event: QGraphicsSceneDragDropEvent) -> None:
def operation_dragMoveEvent(
self, event: QGraphicsSceneDragDropEvent
) -> None:
...
def comp_dragLeaveEvent(self, event: QGraphicsSceneDragDropEvent) -> None:
def operation_dragLeaveEvent(
self, event: QGraphicsSceneDragDropEvent
) -> None:
...
def comp_dropEvent(self, event: QGraphicsSceneDragDropEvent) -> None:
def operation_dropEvent(self, event: QGraphicsSceneDragDropEvent) -> None:
...
def comp_hoverEnterEvent(self, event: QGraphicsSceneHoverEvent) -> None:
def operation_hoverEnterEvent(
self, event: QGraphicsSceneHoverEvent
) -> None:
...
def comp_hoverMoveEvent(self, event: QGraphicsSceneHoverEvent) -> None:
def operation_hoverMoveEvent(
self, event: QGraphicsSceneHoverEvent
) -> None:
...
def comp_hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent) -> None:
def operation_hoverLeaveEvent(
self, event: QGraphicsSceneHoverEvent
) -> None:
...
def comp_mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
def operation_mouseMoveEvent(
self, event: QGraphicsSceneMouseEvent
) -> None:
"""
Set the position of the graphical element in the graphic scene,
translate coordinates of the cursor within the graphic element in the
coordinate system of the parent object. The object can only move
horizontally in x-axis scale steps.
coordinate system of the parent object.
"""
def update_pos(item, dx):
pos = item.x() + dx
if self.is_component_valid_pos(item, pos):
item.setX(pos)
def update_pos(operation_item, dx, dy):
pos_x = operation_item.x() + dx
pos_y = operation_item.y() + dy * (
OPERATION_GAP + OPERATION_HEIGHT
)
if self.is_component_valid_pos(operation_item, pos_x):
operation_item.setX(pos_x)
operation_item.setY(pos_y)
self._current_pos.setX(self._current_pos.x() + dx)
self._redraw_lines(item)
self._current_pos.setY(self._current_pos.y() + dy)
self._redraw_lines(operation_item)
self._schedule._y_locations[
operation_item.operation.graph_id
] += dy
item: OperationItem = self.scene().mouseGrabberItem()
delta_x = (item.mapToParent(event.pos()) - self._current_pos).x()
delta_y = (item.mapToParent(event.pos()) - self._current_pos).y()
delta_y_steps = round(delta_y / (OPERATION_GAP + OPERATION_HEIGHT))
if delta_x > 0.505:
update_pos(item, 1)
update_pos(item, 1, delta_y_steps)
elif delta_x < -0.505:
update_pos(item, -1)
update_pos(item, -1, delta_y_steps)
elif delta_y_steps != 0:
update_pos(item, 0, delta_y_steps)
def comp_mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
def operation_mousePressEvent(
self, event: QGraphicsSceneMouseEvent
) -> None:
"""
Changes the cursor to ClosedHandCursor when grabbing an object and
stores the current position in item's parent coordinates. 'event' will
stores the current position in item's parent coordinates. *event* will
by default be accepted, and this item is then the mouse grabber. This
allows the item to receive future move, release and double-click events.
"""
item: OperationItem = self.scene().mouseGrabberItem()
self._signals.component_selected.emit(item.op_id)
self._signals.component_selected.emit(item.graph_id)
self._current_pos = item.mapToParent(event.pos())
self.set_item_active(item)
event.accept()
def comp_mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
def operation_mouseReleaseEvent(
self, event: QGraphicsSceneMouseEvent
) -> None:
"""Change the cursor to OpenHandCursor when releasing an object."""
item: OperationItem = self.scene().mouseGrabberItem()
self.set_item_inactive(item)
self.set_new_starttime(item)
pos = item.x()
self.set_new_start_time(item)
pos_x = item.x()
redraw = False
if pos < 0:
pos += self._schedule.schedule_time
if pos_x < 0:
pos_x += self._schedule.schedule_time
redraw = True
if pos > self._schedule.schedule_time:
pos = pos % self._schedule.schedule_time
if pos_x > self._schedule.schedule_time:
pos_x = pos_x % self._schedule.schedule_time
redraw = True
if redraw:
item.setX(pos)
item.setX(pos_x)
self._redraw_lines(item)
self._signals.component_moved.emit(item.graph_id)
def comp_mouseDoubleClickEvent(
def operation_mouseDoubleClickEvent(
self, event: QGraphicsSceneMouseEvent
) -> None:
...
def comp_wheelEvent(self, event: QGraphicsSceneWheelEvent) -> None:
def operation_wheelEvent(self, event: QGraphicsSceneWheelEvent) -> None:
...
###############################################
#### Event Handlers: GraphicsLineTem ####
###############################################
###################################
# Event Handlers: GraphicsLineTem #
###################################
def timeline_mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
"""
Set the position of the graphical element in the graphic scene,
......@@ -256,13 +292,13 @@ class SchedulerEvent: # PyQt5
horizontally in x-axis scale steps.
"""
def update_pos(item, dx):
pos = item.x() + dx
def update_pos(timeline_item, dx):
pos = timeline_item.x() + dx
if self.is_valid_delta_time(self._delta_time + dx):
item.setX(pos)
timeline_item.setX(pos)
self._current_pos.setX(self._current_pos.x() + dx)
self._delta_time += dx
item.set_text(self._delta_time)
timeline_item.set_text(self._delta_time)
item: TimelineItem = self.scene().mouseGrabberItem()
delta_x = (item.mapToParent(event.pos()) - self._current_pos).x()
......@@ -274,7 +310,8 @@ class SchedulerEvent: # PyQt5
def timeline_mousePressEvent(
self, event: QGraphicsSceneMouseEvent
) -> None:
"""Store the current position in item's parent coordinates. 'event' will
"""
Store the current position in item's parent coordinates. *event* will
by default be accepted, and this item is then the mouse grabber. This
allows the item to receive future move, release and double-click events.
"""
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""B-ASIC Scheduler-gui Graphics Graph Item Module.
"""
B-ASIC Scheduler-gui Graphics Graph Item Module.
Contains the scheduler-gui SchedulerItem class for drawing and
maintain a component in a graph.
......@@ -17,6 +18,11 @@ from qtpy.QtWidgets import QGraphicsItem, QGraphicsItemGroup
from b_asic.operation import Operation
from b_asic.port import InputPort
from b_asic.schedule import Schedule
from b_asic.scheduler_gui._preferences import (
OPERATION_GAP,
OPERATION_HEIGHT,
SCHEDULE_INDENT,
)
from b_asic.scheduler_gui.axes_item import AxesItem
from b_asic.scheduler_gui.operation_item import OperationItem
from b_asic.scheduler_gui.scheduler_event import SchedulerEvent
......@@ -30,11 +36,19 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5
AxesItem, as well as components from OperationItem. It
also inherits from SchedulerEvent, which acts as a filter for events
to OperationItem objects.
Parameters
==========
schedule : Schedule
The Schedule to draw.
parent : QGraphicsItem, optional
The parent. Passed to the constructor of QGraphicsItemGroup
"""
_axes: Optional[AxesItem]
_components: List[OperationItem]
_components_height: float
_operation_items: Dict[str, OperationItem]
_x_axis_indent: float
_event_items: List[QGraphicsItem]
_signal_dict: Dict[OperationItem, Set[SignalItem]]
......@@ -42,7 +56,8 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5
def __init__(
self, schedule: Schedule, parent: Optional[QGraphicsItem] = None
):
"""Constructs a SchedulerItem. 'parent' is passed to QGraphicsItemGroup's constructor.
"""
Construct a SchedulerItem. *parent* is passed to QGraphicsItemGroup's constructor.
"""
# QGraphicsItemGroup.__init__(self, self)
# SchedulerEvent.__init__(self)
......@@ -53,15 +68,15 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5
# super().__init__(parent=self)
self._schedule = schedule
self._axes = None
self._components = []
self._components_height = 0.0
self._x_axis_indent = 0.2
self._operation_items = {}
self._x_axis_indent = SCHEDULE_INDENT
self._event_items = []
self._signal_dict = defaultdict(set)
self._make_graph()
def clear(self) -> None:
"""Sets all children's parent to 'None' and delete the children objects.
"""
Set all children's parent to 'None' and delete the children objects.
"""
self._event_items = []
for item in self.childItems():
......@@ -69,14 +84,26 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5
del item
def is_component_valid_pos(self, item: OperationItem, pos: float) -> bool:
"""Takes in a component position and returns true if the component's new
position is valid, false otherwise."""
"""
Take in a component position and return True if the component's new
position is valid, False otherwise.
Parameters
==========
item : OperationItem
The component.
pos : float
The x-position to check.
"""
# TODO: implement
assert self.schedule is not None, "No schedule installed."
if self.schedule is None:
raise ValueError("No schedule installed.")
end_time = item.end_time
new_start_time = floor(pos) - floor(self._x_axis_indent)
slacks = self.schedule.slacks(item.op_id)
op_start_time = self.schedule.start_time_of_operation(item.op_id)
slacks = self.schedule.slacks(item.graph_id)
op_start_time = self.schedule.start_time_of_operation(item.graph_id)
if not -slacks[0] <= new_start_time - op_start_time <= slacks[1]:
# Cannot move due to dependencies
return False
......@@ -95,54 +122,81 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5
return True
def _redraw_lines(self, item: OperationItem):
def _redraw_all_lines(self) -> None:
"""Redraw all lines in schedule."""
s = set()
for signals in self._signal_dict.values():
s.update(signals)
for signal in s:
signal.update_path()
def _redraw_lines(self, item: OperationItem) -> None:
"""Update lines connected to *item*."""
for signal in self._signal_dict[item]:
signal.update_path()
def set_item_active(self, item: OperationItem):
def set_item_active(self, item: OperationItem) -> None:
"""
Set an item as active, i.e., draw it and connecting signals in special colors.
Parameters
----------
item : OperationItem
The item to set as active.
"""
item.set_active()
for signal in self._signal_dict[item]:
signal.set_active()
def set_item_inactive(self, item: OperationItem):
def set_item_inactive(self, item: OperationItem) -> None:
"""
Set an item as inactive, i.e., draw it and connecting signals in standard colors.
Parameters
----------
item : OperationItem
The item to set as active.
"""
item.set_inactive()
for signal in self._signal_dict[item]:
signal.set_inactive()
def set_new_starttime(self, item: OperationItem) -> None:
"""Set new starttime for *item*."""
def set_new_start_time(self, item: OperationItem) -> None:
"""Set new start time for *item*."""
pos = item.x()
op_start_time = self.schedule.start_time_of_operation(item.op_id)
op_start_time = self.schedule.start_time_of_operation(item.graph_id)
new_start_time = floor(pos) - floor(self._x_axis_indent)
move_time = new_start_time - op_start_time
if move_time:
self.schedule.move_operation(item.op_id, move_time)
self.schedule.move_operation(item.graph_id, move_time)
def is_valid_delta_time(self, delta_time: int) -> bool:
"""Takes in a delta time and returns true if the new schedule time is
valid, false otherwise."""
"""
Takes in a delta time and returns True if the schedule time can be changed by *delta_time*. False otherwise.
"""
# TODO: implement
# item = self.scene().mouseGrabberItem()
assert self.schedule is not None, "No schedule installed."
if self.schedule is None:
raise ValueError("No schedule installed.")
return (
self.schedule.schedule_time + delta_time
>= self.schedule.get_max_end_time()
)
def set_schedule_time(self, delta_time: int) -> None:
"""Set the schedule time and redraw the graph."""
"""Change the schedule time by *delta_time* and redraw the graph."""
if self._axes is None:
raise RuntimeError("No AxesItem!")
assert self.schedule is not None, "No schedule installed."
if self.schedule is None:
raise ValueError("No schedule installed.")
self.schedule.set_schedule_time(
self.schedule.schedule_time + delta_time
)
self._axes.set_width(self._axes.width + delta_time)
# Redraw all lines
for signals in self._signal_dict.values():
for signal in signals:
signal.update_path()
self._redraw_all_lines()
@property
def schedule(self) -> Schedule:
......@@ -155,62 +209,84 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5
@property
def components(self) -> List[OperationItem]:
return self._components
return list(component for component in self._operation_items.values())
@property
def event_items(self) -> List[QGraphicsItem]:
"""Return a list of objects that receives events."""
return self._event_items
def _make_graph(self) -> None:
"""Makes a new graph out of the stored attributes."""
# build components
spacing = 0.2
_components_dict = {}
# print('Start times:')
for op_id, op_start_time in self.schedule.start_times.items():
operation = cast(Operation, self.schedule.sfg.find_by_id(op_id))
# if not isinstance(op, (Input, Output)):
self._components_height += spacing
component = OperationItem(operation, parent=self)
component.setPos(
self._x_axis_indent + op_start_time, self._components_height
def _set_position(self, graph_id) -> None:
op_item = self._operation_items[graph_id]
op_item.setPos(
self._x_axis_indent + self.schedule.start_times[graph_id],
self.schedule._get_y_position(
graph_id, OPERATION_HEIGHT, OPERATION_GAP
),
)
def _redraw_from_start(self) -> None:
self.schedule._reset_y_locations()
for graph_id in {
k: v
for k, v in sorted(
self.schedule.start_times.items(), key=lambda item: item[1]
)
self._components.append(component)
_components_dict[operation] = component
self._components_height += component.height
self._event_items += component.event_items
# self._components_height += spacing
}:
self._set_position(graph_id)
self._redraw_all_lines()
def _update_axes(self, build=False) -> None:
# build axes
schedule_time = self.schedule.schedule_time
self._axes = AxesItem(
schedule_time, int(self._components_height - spacing)
max_pos_graph_id = max(
self.schedule._y_locations, key=self.schedule._y_locations.get
)
self._axes.setPos(0, self._components_height + spacing * 2)
self._event_items += self._axes.event_items
y_pos_min = self.schedule._get_y_position(
max_pos_graph_id, OPERATION_HEIGHT, OPERATION_GAP
)
if self._axes is None or build:
self._axes = AxesItem(schedule_time, y_pos_min + 0.5)
self._event_items += self._axes.event_items
else:
self._axes.set_height(y_pos_min + 0.5)
self._axes.setPos(0, y_pos_min + OPERATION_HEIGHT + OPERATION_GAP)
def _make_graph(self) -> None:
"""Make a new graph out of the stored attributes."""
# build components
for graph_id in self.schedule.start_times.keys():
operation = cast(Operation, self.schedule.sfg.find_by_id(graph_id))
component = OperationItem(
operation, height=OPERATION_HEIGHT, parent=self
)
self._operation_items[graph_id] = component
self._set_position(graph_id)
self._event_items += component.event_items
# self._axes.width = schedule_time
# add axes and components
self._update_axes(build=True)
self.addToGroup(self._axes)
# self._axes.update_axes(schedule_time - 2, self._components_height, self._x_axis_indent)
for component in self._components:
for component in self.components:
self.addToGroup(component)
# self.addToGroup(self._components)
# add signals
for component in self._components:
for component in self.components:
for output_port in component.operation.outputs:
for signal in output_port.signals:
destination = cast(InputPort, signal.destination)
dest_component = _components_dict[destination.operation]
destination_component = self._operation_items[
destination.operation.graph_id
]
gui_signal = SignalItem(
component, dest_component, signal, parent=self
component, destination_component, signal, parent=self
)
self.addToGroup(gui_signal)
self._signal_dict[component].add(gui_signal)
self._signal_dict[dest_component].add(gui_signal)
self._signal_dict[destination_component].add(gui_signal)
pprint(SchedulerItem.__mro__)