diff --git a/b_asic/scheduler-gui/graphics_axes_item.py b/b_asic/scheduler-gui/graphics_axes_item.py index 712e6cfb6bc4b0cbb17fed704795aa9751205eb4..c8f8476ae3d6564db619975ccce69b23a8035dc4 100644 --- a/b_asic/scheduler-gui/graphics_axes_item.py +++ b/b_asic/scheduler-gui/graphics_axes_item.py @@ -9,8 +9,8 @@ import os import sys from typing import Any, Optional from pprint import pprint -from typing import Any, Union, Optional, overload, Final, final, Dict, List -# from typing_extensions import Self, Final, Literal, LiteralString, TypeAlias, final +from typing import Any, Union, Optional, overload, Dict, List, TypeAlias +from typing_extensions import Self import numpy as np from copy import deepcopy from math import cos, sin, pi @@ -42,38 +42,68 @@ from graphics_timeline_item import GraphicsTimelineItem class GraphicsAxesItem(QGraphicsItemGroup): """A class to represent axes in a graph.""" - _scale: float = 1.0 + _scale: float = 1.0 """Static, changed from MainWindow.""" - _width: float - _width_padding: float - _height: float - _dy_height: float - _x_indent: float - _axes: Dict[str, Union[QGraphicsItemGroup, QGraphicsLineItem]] - _event_items: List[QGraphicsItem] - _timeline: GraphicsTimelineItem + _width: int + _width_indent: float + _width_padding: float + _height: int + _height_indent: float + _height_padding: float + _x_axis: QGraphicsLineItem + _x_label: QGraphicsSimpleTextItem + _x_arrow: QGraphicsPolygonItem + _x_scale: List[QGraphicsLineItem] + _x_scale_labels: List[QGraphicsSimpleTextItem] + _x_ledger: List[Union[QGraphicsLineItem, GraphicsTimelineItem]] + _x_label_offset: float + _y_axis: QGraphicsLineItem + _event_items: List[QGraphicsItem] + _base_pen: QPen + _ledger_pen: QPen + _timeline_pen: QPen - def __init__(self, width: float, height: float, x_indent: float = 0.2, parent: Optional[QGraphicsItem] = None): + def __init__(self, width: int, height: int, + width_indent: Optional[float] = 0.2, height_indent: Optional[float] = 0.2, + width_padding: Optional[float] = 0.6, height_padding: Optional[float] = 0.5, + parent: Optional[QGraphicsItem] = None): """Constructs a GraphicsAxesItem. 'parent' is passed to QGraphicsItemGroup's constructor.""" super().__init__(parent) + assert width >= 0, f"'width' greater or equal to 0 expected, got: {width}." + assert height >= 0, f"'height' greater or equal to 0 expected, got: {height}." - self._width = width - self._width_padding = 0.6 - self._padded_width = width + self._width_padding - self._height = height - self._dy_height = 5/self._scale - self._x_indent = x_indent - self._axes = {} - self._event_items = [] - # self._timeline = GraphicsTimelineItem() - # self._event_items.append(self._timeline) - - self._make_axes() + self._width = width + self._height = height + self._width_indent = width_indent + self._height_indent = height_indent + self._width_padding = width_padding + self._height_padding = height_padding + self._x_axis = QGraphicsLineItem() + self._x_label = QGraphicsSimpleTextItem() + self._x_arrow = QGraphicsPolygonItem() + self._x_scale = [] + self._x_scale_labels = [] + self._x_ledger = [] + self._x_label_offset = 0.2 + self._y_axis = QGraphicsLineItem() + self._event_items = [] + + self._base_pen = QPen() + self._base_pen.setWidthF(2/self._scale) + self._base_pen.setJoinStyle(Qt.MiterJoin) + self._ledger_pen = QPen(Qt.lightGray) + self._ledger_pen.setWidthF(0) # 0 = cosmetic pen 1px width + self._timeline_pen = QPen(Qt.black) + self._timeline_pen.setWidthF(2/self._scale) + self._timeline_pen.setStyle(Qt.DashLine) + + self._make_base() def clear(self) -> None: """Sets all children's parent to 'None' and delete the axes.""" + # TODO: update, needed? # self._timeline.setParentItem(None) self._event_items = [] keys = list(self._axes.keys()) @@ -83,144 +113,178 @@ class GraphicsAxesItem(QGraphicsItemGroup): @property - def width(self) -> float: + def width(self) -> int: """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 - def width(self, width: float) -> None: - if self._width != width: - self.update_axes(width = width) + # @width.setter + # def width(self, width: int) -> None: + # if self._width != width: + # self.update_axes(width = width) @property - def height(self) -> float: + def height(self) -> int: """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 - def height(self, height: float) -> 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 x_indent(self) -> float: - """Get or set the current x-axis indent. Setting the indent to a new - value will update the axes automatically.""" - return self._x_indent - @x_indent.setter - def x_indent(self, x_indent: float) -> None: - if self._x_indent != x_indent: - self.update_axes(x_indent = x_indent) + # @property + # def width_indent(self) -> float: + # """Get or set the current x-axis indent. Setting the indent to a new + # value will update the axes automatically.""" + # return self._width_indent + # @width_indent.setter + # def width_indent(self, width_indent: float) -> None: + # if self._width_indent != width_indent: + # self.update_axes(width_indent = width_indent) @property def event_items(self) -> List[QGraphicsItem]: """Returnes a list of objects, that receives events.""" - return self._event_items + return [self._x_ledger[-1]] - @property - def timeline(self) -> GraphicsTimelineItem: - return self._timeline - def _register_event_item(self, item: QGraphicsItem) -> None: """Register an object that receives events.""" - # item.setFlag(QGraphicsItem.ItemIsMovable) # mouse move events - # item.setAcceptHoverEvents(True) # mouse hover events - # item.setAcceptedMouseButtons(Qt.LeftButton) # accepted buttons for movements self._event_items.append(item) + def set_height(self, height: int) -> "Self": + # TODO: implement, docstring + raise NotImplemented + return self + + def set_width(self, width: int) -> "Self": + # TODO: docstring + assert width >= 0, f"'width' greater or equal to 0 expected, got: {width}." + delta_width = width - self._width + + if delta_width > 0: + for _ in range(delta_width): + self._append_x_tick() + elif delta_width < 0: + for _ in range(abs(delta_width)): + self._pop_x_tick() + self._width = width + + return self + + def _pop_x_tick(self) -> None: + # TODO: docstring + + # remove the next to last x_scale, x_scale_labels and x_ledger + x_scale = self._x_scale.pop(-2) + x_scale_labels = self._x_scale_labels.pop(-2) + x_ledger = self._x_ledger.pop(-2) + x_scale.setParentItem(None) + x_scale_labels.setParentItem(None) + x_ledger.setParentItem(None) + del x_scale + del x_scale_labels + del x_ledger + + # move timeline x_scale and x_scale_labels (timeline already moved by event) + self._x_scale[-1].setX(self._x_scale[-1].x() - 1) + self._x_scale_labels[-1].setX(self._x_scale_labels[-1].x() - 1) + self._x_scale_labels[-1].setText(str(len(self._x_scale) - 1)) + + # move arrow, x-axis label and decrease x-axis + self._x_arrow.setX(self._x_arrow.x() - 1) + self._x_label.setX(self._x_label.x() - 1) + self._x_axis.setLine(0, 0, self._width_indent + self._width-1 + self._width_padding, 0) + + + + def _append_x_tick(self) -> None: + # TODO: docstring + + index = len(self._x_scale) + is_timeline = True + if index != 0: + index -= 1 + is_timeline = False + + ## 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) + pos = self.mapToScene(QPointF(self._width_indent + index, 0)) + self._x_scale[index].setPos(pos) + self.addToGroup(self._x_scale[index]) + + # x-axis scale number + self._x_scale_labels.insert(index, QGraphicsSimpleTextItem(str(index))) + self._x_scale_labels[index].setScale(1 / self._scale) + x_pos = self._width_indent + index + x_pos -= self.mapRectFromItem(self._x_scale_labels[index], self._x_scale_labels[index].boundingRect()).width()/2 + pos = self.mapToScene(QPointF(x_pos, self._x_label_offset)) + self._x_scale_labels[index].setPos(pos) + self.addToGroup(self._x_scale_labels[index]) + + # x-axis vertical ledger + if is_timeline: # last line is a timeline + self._x_ledger.insert(index, GraphicsTimelineItem(0, 0, 0, -(self._height_indent + self._height + self._height_padding))) + self._x_ledger[index].setPen(self._timeline_pen) + self._x_ledger[index].set_text_scale(1.05/self._scale) + self._register_event_item(self._x_ledger[index]) + else: + self._x_ledger.insert(index, QGraphicsLineItem(0, 0, 0, -(self._height_indent + self._height + self._height_padding))) + self._x_ledger[index].setPen(self._ledger_pen) + pos = self.mapToScene(QPointF(self._width_indent + index, 0)) + self._x_ledger[index].setPos(pos) + 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 + if not is_timeline: + # expand x-axis, move arrow and x-axis label + self._x_axis.setLine(0, 0, self._width_indent + index + 1 + self._width_padding, 0) + self._x_arrow.setX(self._x_arrow.x() + 1) + self._x_label.setX(self._x_label.x() + 1) + # move last x-scale and x-scale-label + self._x_scale_labels[index + 1].setX(self._x_scale_labels[index + 1].x() + 1) + 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_axes(self, width: Optional[float] = None, height: Optional[float] = None, x_indent: Optional[float] = None) -> None: - """Updates the current axes with the new 'width', 'height' and 'x_indent'. If any of the - parameters is omitted, the stored value will be used.""" - if width is not None: - self._width = width - self._padded_width = width + self._width_padding - if height is not None: self._height = height - if x_indent is not None: self._x_indent = x_indent - print(width is not None or height is not None or x_indent is not None) - if (width is not None - or height is not None - or x_indent is not None): - self.clear() - self._make_axes() - - - def _make_axes(self) -> None: - """Makes new axes out of the stored attributes.""" - # self.prepareGeometryChange() - ## define pencils - pen = QPen() - pen.setWidthF(2/self._scale) - pen.setJoinStyle(Qt.MiterJoin) - ledger_pen = QPen(Qt.lightGray) - ledger_pen.setWidthF(0) # 0 = cosmetic pen 1px width - - ## x-axis - self._axes['x'] = QGraphicsItemGroup() - line = QGraphicsLineItem(0, 0, self._padded_width, 0) - line.setPen(pen) - self._axes['x'].addToGroup(line) + def _make_base(self) -> None: + + # x axis + self._x_axis.setLine(0, 0, self._width_indent + self._width_padding, 0) + self._x_axis.setPen(self._base_pen) + self.addToGroup(self._x_axis) + # x-axis arrow arrow_size = 8/self._scale p0 = QPointF(0, sin(pi/6) * arrow_size) p1 = QPointF(arrow_size, 0) p2 = QPointF(0, -sin(pi/6) * arrow_size) polygon = QPolygonF([p0, p1, p2]) - arrow = QGraphicsPolygonItem(polygon) - arrow.setPen(pen) - arrow.setBrush(QBrush(Qt.SolidPattern)) - arrow.setPos(self._padded_width, 0) - self._axes['x'].addToGroup(arrow) - # x-axis scale - x_scale = [] - x_scale_labels = [] - x_ledger = [] - self._axes['x_ledger'] = QGraphicsItemGroup() - for i in range(int(self._padded_width) + 1): - x_pos = QPointF(self.x_indent + i, 0) - # vertical x-scale - x_scale.append(QGraphicsLineItem(0, 0, 0, 0.05)) - x_scale[i].setPen(pen) - x_scale[i].setPos(x_pos) - self._axes['x'].addToGroup(x_scale[i]) - # numbers - x_scale_labels.append(QGraphicsSimpleTextItem(str(i))) - x_scale_labels[i].setScale(x_scale_labels[i].scale() / self._scale) - center = x_pos - self.mapFromItem(x_scale_labels[i], x_scale_labels[i].boundingRect().center()) - x_scale_labels[i].setPos(center + QPointF(0, 0.2)) - self._axes['x'].addToGroup(x_scale_labels[i]) - # vertical x-ledger - if i == int(self.width): # last line is a timeline - ledger_pen.setWidthF(2/self._scale) - ledger_pen.setStyle(Qt.DashLine) - ledger_pen.setColor(Qt.black) - # self._timeline.setLine(0, 0, 0, self.height) - # x_ledger.append(self._timeline) - self._timeline = GraphicsTimelineItem(0, 0, 0, self.height) - self._timeline.set_text_scale(1.05/self._scale) - x_ledger.append(self._timeline) - self._register_event_item(x_ledger[i]) - else: - x_ledger.append(QGraphicsLineItem(0, 0, 0, self.height)) - x_ledger[i].setPen(ledger_pen) - x_ledger[i].setPos(x_pos) - self._axes['x_ledger'].addToGroup(x_ledger[i]) + self._x_arrow.setPolygon(polygon) + self._x_arrow.setPen(self._base_pen) + self._x_arrow.setBrush(QBrush(Qt.SolidPattern)) + self._x_arrow.setPos(self._width_indent + self._width_padding, 0) + self.addToGroup(self._x_arrow) + # x-axis label - label = QGraphicsSimpleTextItem('time') - label.setScale(label.scale() / self._scale) - center = self.mapFromItem(arrow, arrow.boundingRect().center()) # =center of arrow - center -= self.mapFromItem(label, label.boundingRect().center()) # -center of label - label.setPos(center + QPointF(0, 0.2)) # move down under arrow - self._axes['x'].addToGroup(label) - + self._x_label.setText('time') + self._x_label.setScale(1 / self._scale) + x_pos = self._width_indent + 0 + self._width_padding # end of x-axis + x_pos += self.mapRectFromItem(self._x_arrow, self._x_arrow.boundingRect()).width()/2 # + half arrow width + x_pos -= self.mapRectFromItem(self._x_label, self._x_label.boundingRect()).width()/2 # - center of label + self._x_label.setPos(x_pos, self._x_label_offset) + self.addToGroup(self._x_label) + + # x-axis timeline + self._append_x_tick() + for _ in range(self._width): + self._append_x_tick() + pos = self._x_ledger[-1].pos() + self._x_ledger[-1].setPos(pos + QPoint(self._width, 0)) # move timeline + # y-axis - self._axes['y'] = QGraphicsLineItem(0, 0, 0, self.height + self._dy_height) - self._axes['y'].setPen(pen) - - # put it all together - self._axes['x_ledger'].setPos(0, self._dy_height) - self.addToGroup(self._axes['x_ledger']) - self._axes['x'].setPos(0, self.height + self._dy_height) - self.addToGroup(self._axes['x']) - self.addToGroup(self._axes['y']) - \ No newline at end of file + 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.addToGroup(self._y_axis) diff --git a/b_asic/scheduler-gui/graphics_graph_event.py b/b_asic/scheduler-gui/graphics_graph_event.py index 4819db35d6957346cc5e4a48015b15ca2028986c..857b65a699c149e2086207345b9e2244a46bafc9 100644 --- a/b_asic/scheduler-gui/graphics_graph_event.py +++ b/b_asic/scheduler-gui/graphics_graph_event.py @@ -63,6 +63,7 @@ class GraphicsGraphEvent(QGraphicsItem): """Installs an event filter for 'filterItems' on 'self', causing all events for 'filterItems' to first pass through 'self's sceneEventFilter() function. 'filterItems' can be one object or a list of objects.""" + item: GraphicsComponentItem for item in filterItems: item.installSceneEventFilter(self) @@ -73,6 +74,7 @@ class GraphicsGraphEvent(QGraphicsItem): def removeSceneEventFilters(self, filterItems) -> None: """Removes an event filter on 'filterItems' from 'self'. 'filterItems' can be one object or a list of objects.""" + item: GraphicsComponentItem for item in filterItems: item.removeSceneEventFilter(self) @@ -206,7 +208,7 @@ class GraphicsGraphEvent(QGraphicsItem): horizontally in x-axis scale steps.""" # Qt.DragMoveCursor # button = event.button() - item = self.scene().mouseGrabberItem() + item: GraphicsTimelineItem = self.scene().mouseGrabberItem() dx = (item.mapToParent(event.pos()) - self._current_pos).x() if dx > 0.505: pos = item.x() + 1.0 @@ -215,7 +217,7 @@ class GraphicsGraphEvent(QGraphicsItem): item.setX(pos) self._current_pos.setX(self._current_pos.x() + 1.0) self._delta_time += 1 - self._axes.timeline.set_text(self._delta_time) + item.set_text(self._delta_time) elif dx < -0.505: pos = item.x() - 1.0 if self.is_valid_delta_time(self._delta_time - 1): @@ -223,22 +225,23 @@ class GraphicsGraphEvent(QGraphicsItem): item.setX(pos) self._current_pos.setX(self._current_pos.x() - 1.0) self._delta_time -= 1 - self._axes.timeline.set_text(self._delta_time) + item.set_text(self._delta_time) def timeline_mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: """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 = self.scene().mouseGrabberItem() - self._current_pos = item.mapToParent(event.pos()) + item: GraphicsTimelineItem = self.scene().mouseGrabberItem() self._delta_time = 0 - self._axes.timeline.set_text(self._delta_time) - self._axes.timeline.show_label() + item.set_text(self._delta_time) + item.show_label() + self._current_pos = item.mapToParent(event.pos()) event.accept() def timeline_mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: """Updates the schedule time.""" + item: GraphicsTimelineItem = self.scene().mouseGrabberItem() + item.hide_label() if self._delta_time != 0: self.set_schedule_time(self._delta_time) - self._axes.timeline.hide_label() \ No newline at end of file diff --git a/b_asic/scheduler-gui/graphics_graph_item.py b/b_asic/scheduler-gui/graphics_graph_item.py index 684dd1bb3561e261b6a499b7f4890f9763541fe0..eead29563bebf2b33890d3dd652304df8e0c71da 100644 --- a/b_asic/scheduler-gui/graphics_graph_item.py +++ b/b_asic/scheduler-gui/graphics_graph_item.py @@ -101,15 +101,19 @@ class GraphicsGraphItem(QGraphicsItemGroup, GraphicsGraphEvent): """Set the schedule time and redraw the graph.""" assert self.schedule is not None , "No schedule installed." self.schedule.set_schedule_time(self.schedule.schedule_time + delta_time) - scene = self.scene() - if scene is not None: - self.removeSceneEventFilters(self._axes.event_items) - self._axes.update_axes(width = self._axes.width + delta_time) - if scene is not None: - self.installSceneEventFilters(self._axes.event_items) + # scene = self.scene() + # if scene is not None: + # self.removeSceneEventFilters(self._axes.event_items) + # self._axes.update_axes(width = self._axes.width + delta_time) + # if scene is not None: + # self.installSceneEventFilters(self._axes.event_items) # print(f'self._axes.event_items {self._axes.event_items}') # print(f'set_schedule_time({delta_time})') + self._axes.set_width(self._axes.width + delta_time) + # self.scene().views()[0].updateScene(QRectF(0, 0, 1, 1)) + + @property def schedule(self) -> Schedule: return self._schedule @@ -142,11 +146,12 @@ class GraphicsGraphItem(QGraphicsItemGroup, GraphicsGraphEvent): self._components.append(component) self._components_height += component.height self._event_items += component.event_items - self._components_height += spacing + # self._components_height += spacing # build axes schedule_time = self.schedule.schedule_time - self._axes = GraphicsAxesItem(schedule_time, self._components_height, self._x_axis_indent) + self._axes = GraphicsAxesItem(schedule_time, self._components_height - spacing) + self._axes.setPos(0, self._components_height + spacing*2) self._event_items += self._axes.event_items # self._axes.width = schedule_time diff --git a/b_asic/scheduler-gui/graphics_timeline_item.py b/b_asic/scheduler-gui/graphics_timeline_item.py index 982ef75095bab6b62a7d8b1c1e2bcd351a5cb69a..0bfff1608091afe03e13d6e324bab5ebbd4c77b1 100644 --- a/b_asic/scheduler-gui/graphics_timeline_item.py +++ b/b_asic/scheduler-gui/graphics_timeline_item.py @@ -72,7 +72,7 @@ class GraphicsTimelineItem(QGraphicsLineItem): self._delta_time_label.setScale(1.05/75) # TODO: dont hardcode scale self._delta_time_label.setParentItem(self) x_pos = - self._delta_time_label.mapRectToParent(self._delta_time_label.boundingRect()).width()/2 - y_pos = 1.05 * self.line().dy() + y_pos = 0.5 self._delta_time_label.setPos(x_pos, y_pos) # pen = QPen(Qt.black) # self._delta_time_label.setPen(pen) diff --git a/b_asic/scheduler-gui/tests/graphics_axes_item.py b/b_asic/scheduler-gui/tests/graphics_axes_item.py new file mode 100644 index 0000000000000000000000000000000000000000..712e6cfb6bc4b0cbb17fed704795aa9751205eb4 --- /dev/null +++ b/b_asic/scheduler-gui/tests/graphics_axes_item.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""B-ASIC Scheduler-gui Graphics Axes Item Module. + +Contains the scheduler-gui GraphicsAxesItem class for drawing and maintain the axes in a graph. +""" +from operator import contains +import os +import sys +from typing import Any, Optional +from pprint import pprint +from typing import Any, Union, Optional, overload, Final, final, Dict, List +# from typing_extensions import Self, Final, Literal, LiteralString, TypeAlias, final +import numpy as np +from copy import deepcopy +from math import cos, sin, pi + +import qtpy +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + +# QGraphics and QPainter imports +from qtpy.QtCore import ( + Qt, QObject, QRect, QRectF, QPoint, QSize, QSizeF, QByteArray, qAbs) +from qtpy.QtGui import ( + QPaintEvent, QPainter, QPainterPath, QColor, QBrush, QPen, QFont, QPolygon, QIcon, QPixmap, + QLinearGradient, QTransform, QPolygonF) +from qtpy.QtWidgets import ( + QGraphicsView, QGraphicsScene, QGraphicsWidget, + QGraphicsLayout, QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsLayoutItem, QGraphicsAnchorLayout, + QGraphicsItem, QGraphicsItemGroup, QGraphicsPathItem, QGraphicsLineItem, QGraphicsTextItem, QGraphicsRectItem, + QStyleOptionGraphicsItem, QWidget, QGraphicsObject, QGraphicsSimpleTextItem, QGraphicsPolygonItem) +from qtpy.QtCore import ( + QPoint, QPointF) + +# B-ASIC +import logger +from graphics_timeline_item import GraphicsTimelineItem + + + +class GraphicsAxesItem(QGraphicsItemGroup): + """A class to represent axes in a graph.""" + _scale: float = 1.0 + """Static, changed from MainWindow.""" + _width: float + _width_padding: float + _height: float + _dy_height: float + _x_indent: float + _axes: Dict[str, Union[QGraphicsItemGroup, QGraphicsLineItem]] + _event_items: List[QGraphicsItem] + _timeline: GraphicsTimelineItem + + + def __init__(self, width: float, height: float, x_indent: float = 0.2, parent: Optional[QGraphicsItem] = None): + """Constructs a GraphicsAxesItem. 'parent' is passed to QGraphicsItemGroup's constructor.""" + super().__init__(parent) + + self._width = width + self._width_padding = 0.6 + self._padded_width = width + self._width_padding + self._height = height + self._dy_height = 5/self._scale + self._x_indent = x_indent + self._axes = {} + self._event_items = [] + # self._timeline = GraphicsTimelineItem() + # self._event_items.append(self._timeline) + + self._make_axes() + + + def clear(self) -> None: + """Sets all children's parent to 'None' and delete the axes.""" + # self._timeline.setParentItem(None) + self._event_items = [] + keys = list(self._axes.keys()) + for key in keys: + self._axes[key].setParentItem(None) + del self._axes[key] + + + @property + def width(self) -> float: + """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 + def width(self, width: float) -> None: + if self._width != width: + self.update_axes(width = width) + + @property + def height(self) -> float: + """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 + def height(self, height: float) -> None: + if self._height != height: + self.update_axes(height = height) + + @property + def x_indent(self) -> float: + """Get or set the current x-axis indent. Setting the indent to a new + value will update the axes automatically.""" + return self._x_indent + @x_indent.setter + def x_indent(self, x_indent: float) -> None: + if self._x_indent != x_indent: + self.update_axes(x_indent = x_indent) + + @property + def event_items(self) -> List[QGraphicsItem]: + """Returnes a list of objects, that receives events.""" + return self._event_items + + @property + def timeline(self) -> GraphicsTimelineItem: + return self._timeline + + def _register_event_item(self, item: QGraphicsItem) -> None: + """Register an object that receives events.""" + # item.setFlag(QGraphicsItem.ItemIsMovable) # mouse move events + # item.setAcceptHoverEvents(True) # mouse hover events + # item.setAcceptedMouseButtons(Qt.LeftButton) # accepted buttons for movements + self._event_items.append(item) + + + def update_axes(self, width: Optional[float] = None, height: Optional[float] = None, x_indent: Optional[float] = None) -> None: + """Updates the current axes with the new 'width', 'height' and 'x_indent'. If any of the + parameters is omitted, the stored value will be used.""" + if width is not None: + self._width = width + self._padded_width = width + self._width_padding + if height is not None: self._height = height + if x_indent is not None: self._x_indent = x_indent + print(width is not None or height is not None or x_indent is not None) + if (width is not None + or height is not None + or x_indent is not None): + self.clear() + self._make_axes() + + + def _make_axes(self) -> None: + """Makes new axes out of the stored attributes.""" + # self.prepareGeometryChange() + ## define pencils + pen = QPen() + pen.setWidthF(2/self._scale) + pen.setJoinStyle(Qt.MiterJoin) + ledger_pen = QPen(Qt.lightGray) + ledger_pen.setWidthF(0) # 0 = cosmetic pen 1px width + + ## x-axis + self._axes['x'] = QGraphicsItemGroup() + line = QGraphicsLineItem(0, 0, self._padded_width, 0) + line.setPen(pen) + self._axes['x'].addToGroup(line) + # x-axis arrow + arrow_size = 8/self._scale + p0 = QPointF(0, sin(pi/6) * arrow_size) + p1 = QPointF(arrow_size, 0) + p2 = QPointF(0, -sin(pi/6) * arrow_size) + polygon = QPolygonF([p0, p1, p2]) + arrow = QGraphicsPolygonItem(polygon) + arrow.setPen(pen) + arrow.setBrush(QBrush(Qt.SolidPattern)) + arrow.setPos(self._padded_width, 0) + self._axes['x'].addToGroup(arrow) + # x-axis scale + x_scale = [] + x_scale_labels = [] + x_ledger = [] + self._axes['x_ledger'] = QGraphicsItemGroup() + for i in range(int(self._padded_width) + 1): + x_pos = QPointF(self.x_indent + i, 0) + # vertical x-scale + x_scale.append(QGraphicsLineItem(0, 0, 0, 0.05)) + x_scale[i].setPen(pen) + x_scale[i].setPos(x_pos) + self._axes['x'].addToGroup(x_scale[i]) + # numbers + x_scale_labels.append(QGraphicsSimpleTextItem(str(i))) + x_scale_labels[i].setScale(x_scale_labels[i].scale() / self._scale) + center = x_pos - self.mapFromItem(x_scale_labels[i], x_scale_labels[i].boundingRect().center()) + x_scale_labels[i].setPos(center + QPointF(0, 0.2)) + self._axes['x'].addToGroup(x_scale_labels[i]) + # vertical x-ledger + if i == int(self.width): # last line is a timeline + ledger_pen.setWidthF(2/self._scale) + ledger_pen.setStyle(Qt.DashLine) + ledger_pen.setColor(Qt.black) + # self._timeline.setLine(0, 0, 0, self.height) + # x_ledger.append(self._timeline) + self._timeline = GraphicsTimelineItem(0, 0, 0, self.height) + self._timeline.set_text_scale(1.05/self._scale) + x_ledger.append(self._timeline) + self._register_event_item(x_ledger[i]) + else: + x_ledger.append(QGraphicsLineItem(0, 0, 0, self.height)) + x_ledger[i].setPen(ledger_pen) + x_ledger[i].setPos(x_pos) + self._axes['x_ledger'].addToGroup(x_ledger[i]) + # x-axis label + label = QGraphicsSimpleTextItem('time') + label.setScale(label.scale() / self._scale) + center = self.mapFromItem(arrow, arrow.boundingRect().center()) # =center of arrow + center -= self.mapFromItem(label, label.boundingRect().center()) # -center of label + label.setPos(center + QPointF(0, 0.2)) # move down under arrow + self._axes['x'].addToGroup(label) + + # y-axis + self._axes['y'] = QGraphicsLineItem(0, 0, 0, self.height + self._dy_height) + self._axes['y'].setPen(pen) + + # put it all together + self._axes['x_ledger'].setPos(0, self._dy_height) + self.addToGroup(self._axes['x_ledger']) + self._axes['x'].setPos(0, self.height + self._dy_height) + self.addToGroup(self._axes['x']) + self.addToGroup(self._axes['y']) + \ No newline at end of file diff --git a/b_asic/scheduler-gui/tests/graphics_timeline_item.py b/b_asic/scheduler-gui/tests/graphics_timeline_item.py new file mode 100644 index 0000000000000000000000000000000000000000..982ef75095bab6b62a7d8b1c1e2bcd351a5cb69a --- /dev/null +++ b/b_asic/scheduler-gui/tests/graphics_timeline_item.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""B-ASIC Scheduler-gui Graphics Timeline Item Module. + +Contains the a scheduler-gui GraphicsTimelineItem class for drawing and maintain the timeline in a graph. +""" +from operator import contains +import os +import sys +from typing import Any, Optional +from pprint import pprint +from typing import Any, Union, Optional, overload, Final, final, Dict, List +# from typing_extensions import Self, Final, Literal, LiteralString, TypeAlias, final +import numpy as np +from copy import deepcopy +from math import cos, sin, pi + +import qtpy +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + +# QGraphics and QPainter imports +from qtpy.QtCore import ( + Qt, QObject, QRect, QRectF, QPoint, QSize, QSizeF, QByteArray, qAbs, QLineF) +from qtpy.QtGui import ( + QPaintEvent, QPainter, QPainterPath, QColor, QBrush, QPen, QFont, QPolygon, QIcon, QPixmap, + QLinearGradient, QTransform, QPolygonF) +from qtpy.QtWidgets import ( + QGraphicsView, QGraphicsScene, QGraphicsWidget, + QGraphicsLayout, QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsLayoutItem, QGraphicsAnchorLayout, + QGraphicsItem, QGraphicsItemGroup, QGraphicsPathItem, QGraphicsLineItem, QGraphicsTextItem, QGraphicsRectItem, + QStyleOptionGraphicsItem, QWidget, QGraphicsObject, QGraphicsSimpleTextItem, QGraphicsPolygonItem) +from qtpy.QtCore import ( + QPoint, QPointF) + +# B-ASIC +import logger + + + +class GraphicsTimelineItem(QGraphicsLineItem): + """A class to represent the timeline in GraphicsAxesItem.""" + + # _scale: float + _delta_time_label: QGraphicsTextItem + + @overload + def __init__(self, line: QLineF, parent: Optional[QGraphicsItem] = None) -> None: + """Constructs a GraphicsTimelineItem out of 'line'. 'parent' is passed to + QGraphicsLineItem's constructor.""" + ... + @overload + def __init__(self, parent:Optional[QGraphicsItem] = None) -> None: + """Constructs a GraphicsTimelineItem. 'parent' is passed to + QGraphicsLineItem's constructor.""" + ... + @overload + def __init__(self, x1: float, y1: float, x2: float, y2: float, parent:Optional[QGraphicsItem] = None) -> None: + """Constructs a GraphicsTimelineItem from (x1, y1) to (x2, y2). 'parent' is + passed to QGraphicsLineItem's constructor.""" + ... + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setFlag(QGraphicsItem.ItemIsMovable) # mouse move events + self.setAcceptHoverEvents(True) # mouse hover events + self.setAcceptedMouseButtons(Qt.LeftButton) # accepted buttons for movements + + self._delta_time_label = QGraphicsTextItem() + self._delta_time_label.hide() + self._delta_time_label.setScale(1.05/75) # TODO: dont hardcode scale + self._delta_time_label.setParentItem(self) + x_pos = - self._delta_time_label.mapRectToParent(self._delta_time_label.boundingRect()).width()/2 + y_pos = 1.05 * self.line().dy() + self._delta_time_label.setPos(x_pos, y_pos) + # pen = QPen(Qt.black) + # self._delta_time_label.setPen(pen) + + + # @property + # def label(self) -> None: + # return self._delta_time_label + + def set_text(self, number: int) -> None: + """Set the label text to 'number'.""" + # self.prepareGeometryChange() + self._delta_time_label.setPlainText(f'( {number:+} )') + self._delta_time_label.setX(- self._delta_time_label.mapRectToParent(self._delta_time_label.boundingRect()).width()/2) + + # def set_text_pen(self, pen: QPen) -> None: + # """Set the label pen to 'pen'.""" + # self._delta_time_label.setPen(pen) + + # def set_label_visible(self, visible: bool) -> None: + # """If visible is True, the item is made visible. Otherwise, the item is + # made invisible""" + # self._delta_time_label.setVisible(visible) + + def show_label(self) -> None: + """Show the label (label are not visible by default). This convenience + function is equivalent to calling set_label_visible(True).""" + self._delta_time_label.show() + + def hide_label(self) -> None: + """Hide the label (label are not visible by default). This convenience + function is equivalent to calling set_label_visible(False).""" + self._delta_time_label.hide() + + def set_text_scale(self, scale: float) -> None: + self._delta_time_label.setScale(scale) + + @property + def event_items(self) -> List[QGraphicsItem]: + """Returnes a list of objects, that receives events.""" + return [self]