class ParticipantItem(QGraphicsItem): def __init__(self, model_item: Participant, parent=None): super().__init__(parent) self.model_item = model_item self.text = QGraphicsTextItem(self) self.line = QGraphicsLineItem(self) self.line.setPen( QPen(Qt.darkGray, 1, Qt.DashLine, Qt.RoundCap, Qt.RoundJoin)) self.refresh() def update_position(self, x_pos=-1, y_pos=-1): if x_pos == -1: x_pos = self.x_pos() if y_pos == -1: y_pos = self.line.line().y2() self.text.setPos(x_pos - (self.text.boundingRect().width() / 2), 0) self.line.setLine(x_pos, 30, x_pos, y_pos) def x_pos(self): return self.line.line().x1() def width(self): return self.boundingRect().width() def refresh(self): self.text.setPlainText( "?" if not self.model_item else self.model_item.shortname) if hasattr(self.model_item, "simulate") and self.model_item.simulate: font = QFont() font.setBold(True) self.text.setFont(font) self.text.setDefaultTextColor(Qt.darkGreen) self.line.setPen( QPen(Qt.darkGreen, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) else: self.text.setFont(QFont()) self.text.setDefaultTextColor(constants.LINECOLOR) self.line.setPen( QPen(Qt.darkGray, 1, Qt.DashLine, Qt.RoundCap, Qt.RoundJoin)) def boundingRect(self): return self.childrenBoundingRect() def paint(self, painter, option, widget): pass
class WayPoint: def __init__(self, **kwargs): super().__init__() self.location = MapPoint() self.__dict__.update(kwargs) self.pixmap = QGraphicsPixmapItem( QPixmap('HOME_DIR + /nparse/data/maps/waypoint.png')) self.pixmap.setOffset(-10, -20) self.line = QGraphicsLineItem(0.0, 0.0, self.location.x, self.location.y) self.line.setPen(QPen(Qt.green, 1, Qt.DashLine)) self.line.setVisible(False) self.pixmap.setZValue(5) self.line.setZValue(4) self.pixmap.setPos(self.location.x, self.location.y) def update_(self, scale, location=None): self.pixmap.setScale(scale) if location: line = self.line.line() line.setP1(QPointF(location.x, location.y)) self.line.setLine(line) pen = self.line.pen() pen.setWidth(1 / scale) self.line.setPen(pen) self.line.setVisible(True)
class ParticipantItem(QGraphicsItem): def __init__(self, model_item: Participant, parent=None): super().__init__(parent) self.model_item = model_item self.text = QGraphicsTextItem(self) self.line = QGraphicsLineItem(self) self.line.setPen(QPen(Qt.darkGray, 1, Qt.DashLine, Qt.RoundCap, Qt.RoundJoin)) self.refresh() def update_position(self, x_pos=-1, y_pos=-1): if x_pos == -1: x_pos = self.x_pos() if y_pos == -1: y_pos = self.line.line().y2() self.text.setPos(x_pos - (self.text.boundingRect().width() / 2), 0) self.line.setLine(x_pos, 30, x_pos, y_pos) def x_pos(self): return self.line.line().x1() def width(self): return self.boundingRect().width() def refresh(self): self.text.setPlainText("?" if not self.model_item else self.model_item.shortname) if hasattr(self.model_item, "simulate") and self.model_item.simulate: font = QFont() font.setBold(True) self.text.setFont(font) self.text.setDefaultTextColor(Qt.darkGreen) self.line.setPen(QPen(Qt.darkGreen, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) else: self.text.setFont(QFont()) self.text.setDefaultTextColor(constants.LINECOLOR) self.line.setPen(QPen(Qt.darkGray, 1, Qt.DashLine, Qt.RoundCap, Qt.RoundJoin)) def boundingRect(self): return self.childrenBoundingRect() def paint(self, painter, option, widget): pass
def __init__(self, direction, point): super(displacementConstrain, self).__init__() self.direction = direction self.point = point if 1 in self.direction: line1 = QGraphicsLineItem() line1.setLine(self.point.position.x(), self.point.position.y(), self.point.position.x() - 20, self.point.position.y() + 10) line1.setPen(displacementConstrain.pen) line2 = QGraphicsLineItem() line2.setLine(self.point.position.x(), self.point.position.y(), self.point.position.x() - 20, self.point.position.y() - 10) line2.setPen(displacementConstrain.pen) line3 = QGraphicsLineItem() line3.setLine(line2.line().p2().x(), line2.line().p2().y(), line1.line().p2().x(), line1.line().p2().y()) line3.setPen(self.pen) self.addToGroup(line1) self.addToGroup(line2) self.addToGroup(line3) if 2 in direction: line1 = QGraphicsLineItem() line1.setLine(self.point.position.x(), self.point.position.y(), self.point.position.x() - 10, self.point.position.y() + 20) line1.setPen(displacementConstrain.pen) line2 = QGraphicsLineItem() line2.setLine(self.point.position.x(), self.point.position.y(), self.point.position.x() + 10, self.point.position.y() + 20) line2.setPen(displacementConstrain.pen) self.addToGroup(line1) self.addToGroup(line2) line3 = QGraphicsLineItem() line3.setLine(line2.line().p2().x(), line2.line().p2().y(), line1.line().p2().x(), line1.line().p2().y()) line3.setPen(self.pen) self.addToGroup(line3) if 3 in direction: # TODO: implement z rotation pass
class ParticipantItem(QGraphicsItem): def __init__(self, model_item: Participant, parent=None): super().__init__(parent) self.model_item = model_item self.text = QGraphicsTextItem(self) self.line = QGraphicsLineItem(self) self.line.setPen(QPen(Qt.darkGray, 1, Qt.DashLine, Qt.RoundCap, Qt.RoundJoin)) self.refresh() def update_position(self, x_pos=-1, y_pos=-1): if x_pos == -1: x_pos = self.x_pos() if y_pos == -1: y_pos = self.line.line().y2() self.text.setPos(x_pos - (self.text.boundingRect().width() / 2), 0) self.line.setLine(x_pos, 30, x_pos, y_pos) def x_pos(self): return self.line.line().x1() def width(self): return self.boundingRect().width() def refresh(self): self.text.setPlainText("?" if not self.model_item else self.model_item.shortname) def boundingRect(self): return self.childrenBoundingRect() def paint(self, painter, option, widget): pass
def connect_relation(self, x, y, type): ver_line = QGraphicsLineItem(self.en_shape) horz_line = QGraphicsLineItem(self.en_shape) ver_line.setLine( QLineF(self.rel_orgn.x(), self.rel_orgn.y(), self.rel_orgn.x(), y - self.en_shape.y())) horz_line.setLine( QLineF(ver_line.line().x2(), ver_line.line().y2(), x - self.en_shape.x(), y - self.en_shape.y())) self.rel_orgn.setX(self.rel_orgn.x() + self.orgn_step) if (type == 'p'): ver_line.setPen(QPen(Qt.black, 3, Qt.DashLine)) horz_line.setPen(QPen(Qt.black, 3, Qt.DashLine))
class DiagramScene(QGraphicsScene): insert_item, insert_line, move_item = range(3) item_inserted = pyqtSignal(DiagramItem) def __init__(self, context_menu: QMenu, parent: QGraphicsItem = None): super(DiagramScene, self).__init__(parent) self.context_menu = context_menu self.framework_name = frameworks_utils.get_sorted_frameworks_list()[0] self.mode = self.move_item self.item_type = None self.line = None # Could be used later to support items & lines coloring. self.item_color = Qt.white self.line_color = Qt.black def set_framework_name(self, framework_name: str): self.framework_name = framework_name def set_mode(self, mode: int): self.mode = mode def set_item_type(self, item_type: int): self.item_type = item_type def mousePressEvent(self, event: QGraphicsSceneMouseEvent): if event.button() != Qt.LeftButton: return if self.mode == self.insert_item: layers = frameworks_utils.get_framework_layers(self.framework_name) item = DiagramItem(layers[self.item_type](), self.context_menu) item.setBrush(self.item_color) item.setPos(event.scenePos()) self.addItem(item) self.item_inserted.emit(item) elif self.mode == self.insert_line: self.line = QGraphicsLineItem( QLineF(event.scenePos(), event.scenePos())) self.line.setPen(QPen(self.line_color, 2)) self.addItem(self.line) super(DiagramScene, self).mousePressEvent(event) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == self.insert_line and self.line: new_line = QLineF(self.line.line().p1(), event.scenePos()) self.line.setLine(new_line) elif self.mode == self.move_item: super(DiagramScene, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == self.insert_line and self.line: start_items = self.items(self.line.line().p1()) if len(start_items) and start_items[0] == self.line: start_items.pop(0) end_items = self.items(self.line.line().p2()) if len(end_items) and end_items[0] == self.line: end_items.pop(0) self.removeItem(self.line) self.line = None if len(start_items) and isinstance(start_items[0], QGraphicsSimpleTextItem): start_items[0] = start_items[0].parentItem() if len(end_items) and isinstance(end_items[0], QGraphicsSimpleTextItem): end_items[0] = end_items[0].parentItem() if (len(start_items) and len(end_items) and isinstance(start_items[0], DiagramItem) and isinstance(end_items[0], DiagramItem) and start_items[0] != end_items[0]): start_item = start_items[0] end_item = end_items[0] arrow = Arrow(start_item, end_item) start_item.add_arrow(arrow) end_item.add_arrow(arrow) arrow.setZValue(-1000.0) self.addItem(arrow) arrow.updatePosition() self.line = None super(DiagramScene, self).mouseReleaseEvent(event) def isItemChange(self, type_: DiagramItem) -> bool: for item in self.selectedItems(): if isinstance(item, type_): return True return False
class DiagramScene(QGraphicsScene): InsertItem, InsertLine, InsertText, MoveItem = range(4) itemInserted = pyqtSignal(DiagramItem) textInserted = pyqtSignal(QGraphicsTextItem) itemSelected = pyqtSignal(QGraphicsItem) def __init__(self, itemMenu, parent=None): super(DiagramScene, self).__init__(parent) self.myItemMenu = itemMenu self.myMode = self.MoveItem self.myItemType = DiagramItem.Step self.line = None self.textItem = None self.myItemColor = Qt.white self.myTextColor = Qt.black self.myLineColor = Qt.black self.myFont = QFont() def setLineColor(self, color): self.myLineColor = color if self.isItemChange(Arrow): item = self.selectedItems()[0] item.setColor(self.myLineColor) self.update() def setTextColor(self, color): self.myTextColor = color if self.isItemChange(DiagramTextItem): item = self.selectedItems()[0] item.setDefaultTextColor(self.myTextColor) def setItemColor(self, color): self.myItemColor = color if self.isItemChange(DiagramItem): item = self.selectedItems()[0] item.setBrush(self.myItemColor) def setFont(self, font): self.myFont = font if self.isItemChange(DiagramTextItem): item = self.selectedItems()[0] item.setFont(self.myFont) def setMode(self, mode): self.myMode = mode def setItemType(self, type): self.myItemType = type def editorLostFocus(self, item): cursor = item.textCursor() cursor.clearSelection() item.setTextCursor(cursor) if item.toPlainText(): self.removeItem(item) item.deleteLater() def mousePressEvent(self, mouseEvent): if (mouseEvent.button() != Qt.LeftButton): return if self.myMode == self.InsertItem: item = DiagramItem(self.myItemType, self.myItemMenu) item.setBrush(self.myItemColor) self.addItem(item) item.setPos(mouseEvent.scenePos()) self.itemInserted.emit(item) elif self.myMode == self.InsertLine: self.line = QGraphicsLineItem(QLineF(mouseEvent.scenePos(), mouseEvent.scenePos())) self.line.setPen(QPen(self.myLineColor, 2)) self.addItem(self.line) elif self.myMode == self.InsertText: textItem = DiagramTextItem() textItem.setFont(self.myFont) textItem.setTextInteractionFlags(Qt.TextEditorInteraction) textItem.setZValue(1000.0) textItem.lostFocus.connect(self.editorLostFocus) textItem.selectedChange.connect(self.itemSelected) self.addItem(textItem) textItem.setDefaultTextColor(self.myTextColor) textItem.setPos(mouseEvent.scenePos()) self.textInserted.emit(textItem) super(DiagramScene, self).mousePressEvent(mouseEvent) def mouseMoveEvent(self, mouseEvent): if self.myMode == self.InsertLine and self.line: newLine = QLineF(self.line.line().p1(), mouseEvent.scenePos()) self.line.setLine(newLine) elif self.myMode == self.MoveItem: super(DiagramScene, self).mouseMoveEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): if self.line and self.myMode == self.InsertLine: startItems = self.items(self.line.line().p1()) if len(startItems) and startItems[0] == self.line: startItems.pop(0) endItems = self.items(self.line.line().p2()) if len(endItems) and endItems[0] == self.line: endItems.pop(0) self.removeItem(self.line) self.line = None if len(startItems) and len(endItems) and \ isinstance(startItems[0], DiagramItem) and \ isinstance(endItems[0], DiagramItem) and \ startItems[0] != endItems[0]: startItem = startItems[0] endItem = endItems[0] arrow = Arrow(startItem, endItem) arrow.setColor(self.myLineColor) startItem.addArrow(arrow) endItem.addArrow(arrow) arrow.setZValue(-1000.0) self.addItem(arrow) arrow.updatePosition() self.line = None super(DiagramScene, self).mouseReleaseEvent(mouseEvent) def isItemChange(self, type): for item in self.selectedItems(): if isinstance(item, type): return True return False
class DiagramScene(QGraphicsScene): InsertItem, InsertLine, InsertText, MoveItem, DefaultMode = range(5) itemInserted = pyqtSignal(int) textInserted = pyqtSignal(QGraphicsTextItem) itemSelected = pyqtSignal(QGraphicsItem) def __init__(self, item_menu, parent=None): super(DiagramScene, self).__init__(parent) self.my_item_menu = item_menu self.my_mode = self.DefaultMode self.my_item_type = "channel" self.line = None self.text_item = None self.my_item_color = Qt.white self.my_text_color = Qt.black self.my_line_color = Qt.black self.my_font = QFont() self.m_drag_offset = 0 self.m_dragged = None def set_line_color(self, color): self.my_line_color = color if self.is_item_changed(Arrow): item = self.selectedItems()[0] item.set_color(self.my_line_color) self.update() def set_text_color(self, color): self.my_text_color = color if self.is_item_changed(DiagramTextItem): item = self.selectedItems()[0] item.setDefaultTextColor(self.my_text_color) def set_item_color(self, color): self.my_item_color = color if self.is_item_changed(DiagramTextItem): item = self.selectedItems()[0] item.setBrush(self.my_item_color) def setFont(self, font): self.my_font = font if self.is_item_changed(DiagramTextItem): item = self.selectedItems()[0] item.setFont(self.my_font) def set_mode(self, mode): self.my_mode = mode def set_item_type(self, new_type): self.my_item_type = new_type def editor_lost_focus(self, item): if item: cursor = item.text_cursor() cursor.clearSelection() item.set_text_cursor(cursor) if item.to_plain_text(): self.removeItem(item) item.delete_later() # noinspection PyArgumentList def insert_item(self, item_type=None, x=None, y=None, text=None): if not text: text, ok = QInputDialog.getText(QInputDialog(), 'Insert name', 'Enter new object name:') if not ok: # TODO return item = FlumeObject(item_type, text).pictogram item.setBrush(self.my_item_color) self.addItem(item) item.setPos(x, y) return item def mousePressEvent(self, mouse_event): if mouse_event.button() != Qt.LeftButton: return if self.my_mode == self.InsertItem: x = mouse_event.scenePos().x() # // 50 * 50 y = mouse_event.scenePos().y() # // 50 * 50 item = self.insert_item(self.my_item_type, x, y) self.itemInserted.emit(item.flume_component) elif self.my_mode == self.InsertLine: self.line = QGraphicsLineItem(QLineF(mouse_event.scenePos(), mouse_event.scenePos())) self.line.setPen(QPen(self.my_line_color, 2)) self.addItem(self.line) elif self.my_mode == self.InsertText: text_item = DiagramTextItem() text_item.setFont(self.my_font) text_item.setTextInteractionFlags(Qt.TextEditorInteraction) text_item.setZValue(1000.0) text_item.lostFocus.connect(self.editor_lost_focus) # text_item.selectedChange.connect(self.itemSelected) self.addItem(text_item) text_item.setDefaultTextColor(self.my_text_color) text_item.setPos(mouse_event.scenePos()) self.textInserted.emit(text_item) else: self.m_dragged = QGraphicsScene.itemAt(self, mouse_event.scenePos(), QTransform()) if self.m_dragged: self.my_mode = self.MoveItem self.m_drag_offset = mouse_event.scenePos() - self.m_dragged.pos() super(DiagramScene, self).mousePressEvent(mouse_event) def mouseMoveEvent(self, mouse_event): if self.my_mode == self.InsertLine and self.line: new_line = QLineF(self.line.line().p1(), mouse_event.scenePos()) self.line.setLine(new_line) elif self.my_mode == self.MoveItem: if self.m_dragged: self.m_dragged.setPos(mouse_event.scenePos() - self.m_drag_offset) super(DiagramScene, self).mouseMoveEvent(mouse_event) def mouseReleaseEvent(self, mouse_event): if self.line and self.my_mode == self.InsertLine: start_items = self.items(self.line.line().p1()) if len(start_items) and start_items[0] == self.line: start_items.pop(0) end_items = self.items(self.line.line().p2()) if len(end_items) and end_items[0] == self.line: end_items.pop(0) self.removeItem(self.line) self.line = None if len(start_items) and len(end_items) and isinstance(start_items[0], FlumeDiagramItem) and \ isinstance(end_items[0], FlumeDiagramItem) and start_items[0] != end_items[0]: start_item = start_items[0] end_item = end_items[0] self.add_arrow(start_item, end_item) self.line = None if self.m_dragged: x = mouse_event.scenePos().x() # // 50 * 50 y = mouse_event.scenePos().y() # // 50 * 50 self.m_dragged.setPos(x, y) self.m_dragged = None self.my_mode = self.DefaultMode super(DiagramScene, self).mouseReleaseEvent(mouse_event) def add_arrow(self, start_item, end_item): arrow = Arrow(start_item, end_item) arrow.set_color(self.my_line_color) start_item.add_arrow(arrow) end_item.add_arrow(arrow) arrow.setZValue(-1000.0) self.addItem(arrow) arrow.update_position() def is_item_changed(self, new_type): for item in self.selectedItems(): if isinstance(item, new_type): return True return False
class TransitionGraphicsItem(QGraphicsObject): # constant values SQUARE_SIDE = 10 ARROW_SIZE = 12 PEN_NORMAL_WIDTH = 1 PEN_FOCUS_WIDTH = 3 posChanged = pyqtSignal('QGraphicsItem') def __init__(self, data): super(QGraphicsObject, self).__init__() self.transitionData = data self.originLine = None self.destinationLine = None self.arrow = None self.textGraphics = None self.middleHandle = None self.graphicsOrigin = self.transitionData.origin.getGraphicsItem() self.graphicsDestination = self.transitionData.destination.getGraphicsItem() # connect position changed event self.graphicsOrigin.posChanged.connect(self.statePosChanged) self.graphicsDestination.posChanged.connect(self.statePosChanged) self.midPointX = (self.graphicsDestination.scenePos().x() + self.graphicsOrigin.scenePos().x()) / 2.0 self.midPointY = (self.graphicsDestination.scenePos().y() + self.graphicsOrigin.scenePos().y()) / 2.0 self.createOriginLine() self.createDestinationLine() self.createArrow() self.createMiddleHandle() self.createIdTextBox() def statePosChanged(self, state): if self.graphicsOrigin == state: self.createOriginLine() elif self.graphicsDestination == state: self.createDestinationLine() self.createArrow() def createOriginLine(self): if self.originLine == None: self.originLine = QGraphicsLineItem(self.midPointX, self.midPointY, self.graphicsOrigin.scenePos().x(), self.graphicsOrigin.scenePos().y(), self) else: self.originLine.setLine(QLineF(self.midPointX, self.midPointY, self.graphicsOrigin.scenePos().x(), self.graphicsOrigin.scenePos().y())) myLine = self.originLine.line() myLine.setLength(myLine.length() - StateGraphicsItem.NODE_WIDTH / 2) self.originLine.setLine(myLine) def createDestinationLine(self): if self.destinationLine == None: self.destinationLine = QGraphicsLineItem(self.midPointX, self.midPointY, self.graphicsDestination.scenePos().x(), self.graphicsDestination.scenePos().y(), self) else: self.destinationLine.setLine(QLineF(self.midPointX, self.midPointY, self.graphicsDestination.scenePos().x(), self.graphicsDestination.scenePos().y())) myLine = self.destinationLine.line() myLine.setLength(myLine.length() - StateGraphicsItem.NODE_WIDTH / 2) self.destinationLine.setLine(myLine) def createArrow(self): # add an arrow to destination line myLine = self.destinationLine.line() myLine.setLength(myLine.length() - TransitionGraphicsItem.ARROW_SIZE) rotatePoint = myLine.p2() - self.destinationLine.line().p2() rightPointX = rotatePoint.x() * math.cos(math.pi / 6) - rotatePoint.y() * math.sin(math.pi / 6) rightPointY = rotatePoint.x() * math.sin(math.pi / 6) + rotatePoint.y() * math.cos(math.pi / 6) rightPoint = QPointF(rightPointX + self.destinationLine.line().x2(), rightPointY + self.destinationLine.line().y2()) leftPointX = rotatePoint.x() * math.cos(-math.pi / 6) - rotatePoint.y() * math.sin(-math.pi / 6) leftPointY = rotatePoint.x() * math.sin(-math.pi / 6) + rotatePoint.y() * math.cos(-math.pi / 6) leftPoint = QPointF(leftPointX + self.destinationLine.line().x2(), leftPointY + self.destinationLine.line().y2()) polygon = QPolygonF() polygon << rightPoint << leftPoint << self.destinationLine.line().p2() << rightPoint if self.arrow == None: self.arrow = QGraphicsPolygonItem(polygon, self) else: self.arrow.setPolygon(polygon) brush = QBrush(Qt.SolidPattern) brush.setColor(Qt.black) self.arrow.setBrush(brush) def createMiddleHandle(self): # create middle handle if self.middleHandle == None: self.middleHandle = RectHandleGraphicsItem(TransitionGraphicsItem.SQUARE_SIDE, self) self.middleHandle.setFlag(QGraphicsItem.ItemIsMovable) self.middleHandle.setPos(self.midPointX, self.midPointY) def createIdTextBox(self): if self.textGraphics == None: self.textGraphics = IdTextBoxGraphicsItem(self.transitionData.name, self) self.textGraphics.textChanged.connect(self.nameChanged) else: self.textGraphics.setPlainText(self.transitionData.name) textWidth = self.textGraphics.boundingRect().width() self.textGraphics.setPos(self.midPointX - textWidth / 2, self.midPointY + TransitionGraphicsItem.SQUARE_SIDE - (TransitionGraphicsItem.SQUARE_SIDE / 2) + 5) def updateMiddlePoints(self, newPosition): self.midPointX = newPosition.x() self.midPointY = newPosition.y() self.createOriginLine() self.createDestinationLine() self.createArrow() self.createIdTextBox() self.posChanged.emit(self) def nameChanged(self, name): self.transitionData.name = name self.createIdTextBox() def boundingRect(self): if self.middleHandle != None: return self.middleHandle.boundingRect() else: return None def disableInteraction(self): if self.middleHandle is not None: self.middleHandle.setFlag(QGraphicsItem.ItemIsMovable, False) self.middleHandle.disableInteraction()
class IMUChartView(QChartView): aboutToClose = pyqtSignal(QObject) cursorMoved = pyqtSignal(datetime.datetime) def __init__(self, parent=None): super(QChartView, self).__init__(parent=parent) #self.setFixedHeight(400) #self.setMinimumHeight(500) """self.setMaximumHeight(700) self.setFixedHeight(700) self.setMinimumWidth(1500) self.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed)""" self.reftime = datetime.datetime.now() self.cursor = QGraphicsLineItem() self.scene().addItem(self.cursor) self.decim_factor = 1 # self.setScene(QGraphicsScene()) self.chart = QChart() # self.scene().addItem(self.chart) self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) self.setRubberBand(QChartView.HorizontalRubberBand) # X, Y label on bottom # self.xTextItem = QGraphicsSimpleTextItem(self.chart) # self.xTextItem.setText('X: ') # self.yTextItem = QGraphicsSimpleTextItem(self.chart) # self.yTextItem.setText('Y: ') # self.update_x_y_coords() # Track mouse self.setMouseTracking(True) # Top Widgets newWidget = QWidget(self) newLayout = QHBoxLayout() newLayout.setContentsMargins(0, 0, 0, 0) newWidget.setLayout(newLayout) #labelx = QLabel(self) #labelx.setText('X:') #self.labelXValue = QLabel(self) #labely = QLabel(self) #labely.setText('Y:') #self.labelYValue = QLabel(self) # Test buttons #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) # Spacer #newLayout.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) # Labels """newLayout.addWidget(labelx) newLayout.addWidget(self.labelXValue) self.labelXValue.setMinimumWidth(200) self.labelXValue.setMaximumWidth(200) newLayout.addWidget(labely) newLayout.addWidget(self.labelYValue) self.labelYValue.setMinimumWidth(200) self.labelYValue.setMaximumWidth(200) """ """if parent is not None: parent.layout().setMenuBar(newWidget) """ # self.layout() self.build_style() def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') def closeEvent(self, QCloseEvent): self.aboutToClose.emit(self) @pyqtSlot(QPointF) def lineseries_clicked(self, point): print('lineseries clicked', point) @pyqtSlot(QPointF) def lineseries_hovered(self, point): print('lineseries hovered', point) def update_x_y_coords(self): pass # self.xTextItem.setPos(self.chart.size().width() / 2 - 100, self.chart.size().height() - 40) # self.yTextItem.setPos(self.chart.size().width() / 2 + 100, self.chart.size().height() - 40) def decimate(self, xdata, ydata): assert (len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) #print('decimate factor', decimate_factor) # x = decimate(xdata, decimate_factor) # y = decimate(ydata, decimate_factor) self.decim_factor = decimate_factor x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) for i in range(len(x)): index = i * decimate_factor assert (index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) #print('return size', len(x), len(y), 'timestamp', x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min, max): #print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X # axisX = QDateTimeAxis() # axisX.setTickCount(5) # axisX.setFormat("dd MMM yyyy") # axisX.setTitleText("Date") # self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) axisX = QValueAxis() axisX.setTickCount(10) axisX.setLabelFormat("%li") axisX.setTitleText("Seconds") self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisX) series.attachAxis(axisY) vect = series.pointsVector() for i in range(len(vect)): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable axisX.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) #curve.setUseOpenGL(True) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) for i in range(len(xdecimated)): # TODO hack x = xdecimated[i] - xdecimated[0] curve.append(QPointF(x, ydecimated[i])) self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def set_title(self, title): # print('Setting title: ', title) #self.chart.setTitle(title) pass def series_to_polyline(self, xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) for i in range(0, len(xdata)): polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title("Simple example with %d curves of %d points " \ "(OpenGL Accelerated Series)" \ % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseMoveEvent(e) # Go back to seconds (instead of ms) """xmap = self.chart.mapToValue(e.pos()).x() ymap = self.chart.mapToValue(e.pos()).y() self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) self.labelYValue.setText(str(ymap))""" # self.xTextItem.setText('X: ' + str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) # self.yTextItem.setText('Y: ' + str(ymap)) def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands super().mousePressEvent(e) self.setCursorPosition(e.pos().x(), True) pass def setCursorPosition(self, pos, emit_signal=False): # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap = self.chart.mapToValue(QPointF(pos, 0)).x() ymap = self.chart.mapToValue(QPointF(pos, 0)).y() #self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) #self.labelYValue.setText(str(ymap)) if emit_signal: self.cursorMoved.emit( datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Converts timestamp to x value pos = self.chart.mapToPosition( QPointF((timestamp - self.reftime).total_seconds(), 0)).x() self.setCursorPosition(pos, emit_signal) def mouseReleaseEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseReleaseEvent(e) pass def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # Update cursor height area = self.chart.plotArea() line = self.cursor.line() self.cursor.setLine(line.x1(), area.y(), line.x2(), area.y() + area.height()) # self.scene().setSceneRect(0, 0, e.size().width(), e.size().height()) # Need to reposition X,Y labels self.update_x_y_coords()
class TransitionGraphicsItem(QGraphicsObject): # constant values SQUARE_SIDE = 10 ARROW_SIZE = 12 PEN_NORMAL_WIDTH = 1 PEN_FOCUS_WIDTH = 3 posChanged = pyqtSignal('QGraphicsItem') def __init__(self, data): super(QGraphicsObject, self).__init__() self.transitionData = data self.originLine = None self.destinationLine = None self.arrow = None self.textGraphics = None self.middleHandle = None self.graphicsOrigin = self.transitionData.origin.getGraphicsItem() self.graphicsDestination = self.transitionData.destination.getGraphicsItem( ) # connect position changed event self.graphicsOrigin.posChanged.connect(self.statePosChanged) self.graphicsDestination.posChanged.connect(self.statePosChanged) self.midPointX = (self.graphicsDestination.scenePos().x() + self.graphicsOrigin.scenePos().x()) / 2.0 self.midPointY = (self.graphicsDestination.scenePos().y() + self.graphicsOrigin.scenePos().y()) / 2.0 self.createOriginLine() self.createDestinationLine() self.createArrow() self.createMiddleHandle() self.createIdTextBox() def statePosChanged(self, state): if self.graphicsOrigin == state: self.createOriginLine() elif self.graphicsDestination == state: self.createDestinationLine() self.createArrow() def createOriginLine(self): if self.originLine == None: self.originLine = QGraphicsLineItem( self.midPointX, self.midPointY, self.graphicsOrigin.scenePos().x(), self.graphicsOrigin.scenePos().y(), self) else: self.originLine.setLine( QLineF(self.midPointX, self.midPointY, self.graphicsOrigin.scenePos().x(), self.graphicsOrigin.scenePos().y())) myLine = self.originLine.line() myLine.setLength(myLine.length() - StateGraphicsItem.NODE_WIDTH / 2) self.originLine.setLine(myLine) def createDestinationLine(self): if self.destinationLine == None: self.destinationLine = QGraphicsLineItem( self.midPointX, self.midPointY, self.graphicsDestination.scenePos().x(), self.graphicsDestination.scenePos().y(), self) else: self.destinationLine.setLine( QLineF(self.midPointX, self.midPointY, self.graphicsDestination.scenePos().x(), self.graphicsDestination.scenePos().y())) myLine = self.destinationLine.line() myLine.setLength(myLine.length() - StateGraphicsItem.NODE_WIDTH / 2) self.destinationLine.setLine(myLine) def createArrow(self): # add an arrow to destination line myLine = self.destinationLine.line() myLine.setLength(myLine.length() - TransitionGraphicsItem.ARROW_SIZE) rotatePoint = myLine.p2() - self.destinationLine.line().p2() rightPointX = rotatePoint.x() * math.cos( math.pi / 6) - rotatePoint.y() * math.sin(math.pi / 6) rightPointY = rotatePoint.x() * math.sin( math.pi / 6) + rotatePoint.y() * math.cos(math.pi / 6) rightPoint = QPointF(rightPointX + self.destinationLine.line().x2(), rightPointY + self.destinationLine.line().y2()) leftPointX = rotatePoint.x() * math.cos( -math.pi / 6) - rotatePoint.y() * math.sin(-math.pi / 6) leftPointY = rotatePoint.x() * math.sin( -math.pi / 6) + rotatePoint.y() * math.cos(-math.pi / 6) leftPoint = QPointF(leftPointX + self.destinationLine.line().x2(), leftPointY + self.destinationLine.line().y2()) polygon = QPolygonF() polygon << rightPoint << leftPoint << self.destinationLine.line().p2( ) << rightPoint if self.arrow == None: self.arrow = QGraphicsPolygonItem(polygon, self) else: self.arrow.setPolygon(polygon) brush = QBrush(Qt.SolidPattern) brush.setColor(Qt.black) self.arrow.setBrush(brush) def createMiddleHandle(self): # create middle handle if self.middleHandle == None: self.middleHandle = RectHandleGraphicsItem( TransitionGraphicsItem.SQUARE_SIDE, self) self.middleHandle.setFlag(QGraphicsItem.ItemIsMovable) self.middleHandle.setPos(self.midPointX, self.midPointY) def createIdTextBox(self): if self.textGraphics == None: self.textGraphics = IdTextBoxGraphicsItem(self.transitionData.name, self) self.textGraphics.textChanged.connect(self.nameChanged) else: self.textGraphics.setPlainText(self.transitionData.name) textWidth = self.textGraphics.boundingRect().width() self.textGraphics.setPos( self.midPointX - textWidth / 2, self.midPointY + TransitionGraphicsItem.SQUARE_SIDE - (TransitionGraphicsItem.SQUARE_SIDE / 2) + 5) def updateMiddlePoints(self, newPosition): self.midPointX = newPosition.x() self.midPointY = newPosition.y() self.createOriginLine() self.createDestinationLine() self.createArrow() self.createIdTextBox() self.posChanged.emit(self) def nameChanged(self, name): self.transitionData.name = name self.createIdTextBox() def boundingRect(self): if self.middleHandle != None: return self.middleHandle.boundingRect() else: return None def disableInteraction(self): if self.middleHandle is not None: self.middleHandle.setFlag(QGraphicsItem.ItemIsMovable, False) self.middleHandle.disableInteraction()
class IMUChartView(QChartView, BaseGraph): def __init__(self, parent=None): super().__init__(parent=parent) self.cursor = QGraphicsLineItem() self.scene().addItem(self.cursor) self.xvalues = {} self.chart = QChart() self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) # Selection features # self.setRubberBand(QChartView.HorizontalRubberBand) self.selectionBand = QRubberBand(QRubberBand.Rectangle, self) self.selecting = False self.initialClick = QPoint() # Track mouse self.setMouseTracking(True) self.labelValue = QLabel(self) self.labelValue.setStyleSheet( "background-color: rgba(255,255,255,75%); color: black;") self.labelValue.setAlignment(Qt.AlignCenter) self.labelValue.setMargin(5) self.labelValue.setVisible(False) self.build_style() def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.chart.setTheme(QChart.ChartThemeBlueCerulean) self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') # def closeEvent(self, event): # self.aboutToClose.emit(self) @staticmethod def decimate(xdata, ydata): # assert(len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 # decimate_factor = 1.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) # print('decimate factor', decimate_factor) x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) # for i in range(len(x)): for i, _ in enumerate(x): index = i * decimate_factor # assert(index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min_value, max_value): # print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X axisx = QDateTimeAxis() axisx.setTickCount(5) axisx.setFormat("dd MMM yyyy hh:mm:ss") axisx.setTitleText("Date") self.chart.addAxis(axisx, Qt.AlignBottom) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisx) series.attachAxis(axisY) vect = series.pointsVector() # for i in range(len(vect)): for i, _ in enumerate(vect): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable # axisx.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) # curve.setPointsVisible(True) # curve.setUseOpenGL(True) self.total_samples = max(self.total_samples, len(xdata)) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) # self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) # if len(xdecimated) > 0: # xdecimated = xdecimated - xdecimated[0] xdecimated *= 1000 # No decimal expected points = [] for i, _ in enumerate(xdecimated): # TODO hack # curve.append(QPointF(xdecimated[i], ydecimated[i])) points.append(QPointF(xdecimated[i], ydecimated[i])) curve.replace(points) points.clear() if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) self.xvalues[self.ncurves] = np.array(xdecimated) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def update_data(self, xdata, ydata, series_id): if series_id < len(self.chart.series()): # Find start time to replace data current_series = self.chart.series()[series_id] current_points = current_series.pointsVector() # Find start and end indexes start_index = -1 try: start_index = current_points.index( QPointF(xdata[0] * 1000, ydata[0])) # Right on the value! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x() == xdata[0] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[0] * 1000 < current_points[i].x()): start_index = i # print("update_data: start_index found approximative match.") break end_index = -1 try: end_index = current_points.index( QPointF(xdata[len(xdata) - 1] * 1000, ydata[len(ydata) - 1])) # Right on! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x( ) == xdata[len(xdata) - 1] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[len(xdata) - 1] * 1000 < current_points[i].x()): end_index = i # print("update_data: start_index found approximative match.") break if start_index < 0 or end_index < 0: return # Decimate, if needed xdata, ydata = self.decimate(xdata, ydata) # Check if we have the same number of points for that range. If not, remove and replace! target_points = current_points[start_index:end_index] if len(target_points) != len(xdata): points = [] for i, value in enumerate(xdata): # TODO improve points.append(QPointF(value * 1000, ydata[i])) new_points = current_points[0:start_index] + points[0:len(points)-1] + \ current_points[end_index:len(current_points)-1] current_series.replace(new_points) new_points.clear() # self.xvalues[series_id] = np.array(xdata) return @classmethod def set_title(cls, title): # print('Setting title: ', title) # self.chart.setTitle(title) return @staticmethod def series_to_polyline(xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) # for i in range(0, len(xdata)): # polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) for i, data in enumerate(xdata): polyline[i] = QPointF(data - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title( "Simple example with %d curves of %d points (OpenGL Accelerated Series)" % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): if self.selecting: current_pos = e.pos() if self.interaction_mode == GraphInteractionMode.SELECT: if current_pos.x() < self.initialClick.x(): start_x = current_pos.x() width = self.initialClick.x() - start_x else: start_x = self.initialClick.x() width = current_pos.x() - self.initialClick.x() self.selectionBand.setGeometry( QRect(start_x, self.chart.plotArea().y(), width, self.chart.plotArea().height())) if self.interaction_mode == GraphInteractionMode.MOVE: new_pos = current_pos - self.initialClick self.chart.scroll(-new_pos.x(), new_pos.y()) self.initialClick = current_pos def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands # super().mousePressEvent(e) self.selecting = True self.initialClick = e.pos() if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.setGeometry( QRect(self.initialClick.x(), self.chart.plotArea().y(), 1, self.chart.plotArea().height())) self.selectionBand.show() if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.setOverrideCursor(Qt.ClosedHandCursor) self.labelValue.setVisible(False) def mouseReleaseEvent(self, e: QMouseEvent): # Handling rubberbands clicked_x = self.mapToScene(e.pos()).x() if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.hide() if clicked_x == self.mapToScene(self.initialClick).x(): self.setCursorPosition(clicked_x, True) else: mapped_x = self.mapToScene(self.initialClick).x() if self.initialClick.x() < clicked_x: self.setSelectionArea(mapped_x, clicked_x, True) else: self.setSelectionArea(clicked_x, mapped_x, True) if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.restoreOverrideCursor() self.selecting = False def clearSelectionArea(self, emit_signal=False): self.scene().removeItem(self.selection_rec) self.selection_rec = None if emit_signal: self.clearedSelectionArea.emit() def setSelectionArea(self, start_pos, end_pos, emit_signal=False): selection_brush = QBrush(QColor(153, 204, 255, 128)) selection_pen = QPen(Qt.transparent) self.scene().removeItem(self.selection_rec) self.selection_rec = self.scene().addRect( start_pos, self.chart.plotArea().y(), end_pos - start_pos, self.chart.plotArea().height(), selection_pen, selection_brush) if emit_signal: self.selectedAreaChanged.emit( self.chart.mapToValue(QPointF(start_pos, 0)).x(), self.chart.mapToValue(QPointF(end_pos, 0)).x()) def setSelectionAreaFromTime(self, start_time, end_time, emit_signal=False): # Convert times to x values if isinstance(start_time, datetime.datetime): start_time = start_time.timestamp() * 1000 if isinstance(end_time, datetime.datetime): end_time = end_time.timestamp() * 1000 start_pos = self.chart.mapToPosition(QPointF(start_time, 0)).x() end_pos = self.chart.mapToPosition(QPointF(end_time, 0)).x() self.setSelectionArea(start_pos, end_pos) def setCursorPosition(self, pos, emit_signal=False): # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap_initial = self.chart.mapToValue(QPointF(pos, 0)).x() display = '' # '<i>' + (datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())).strftime('%d-%m-%Y %H:%M:%S') + # '</i><br />' ypos = 10 last_val = None for i in range(self.ncurves): # Find nearest point idx = (np.abs(self.xvalues[i] - xmap_initial)).argmin() ymap = self.chart.series()[i].at(idx).y() xmap = self.chart.series()[i].at(idx).x() if i == 0: display += "<i>" + (datetime.datetime.fromtimestamp(xmap_initial/1000)).strftime('%d-%m-%Y %H:%M:%S:%f') + \ "</i>" # Compute where to display label if last_val is None or ymap > last_val: last_val = ymap ypos = self.chart.mapToPosition(QPointF(xmap, ymap)).y() if display != '': display += '<br />' display += self.chart.series()[i].name( ) + ': <b>' + '%.3f' % ymap + '</b>' self.labelValue.setText(display) self.labelValue.setGeometry(pos, ypos, 100, 100) self.labelValue.adjustSize() self.labelValue.setVisible(True) if emit_signal: self.cursorMoved.emit(xmap_initial) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Find nearest point if isinstance(timestamp, datetime.datetime): timestamp = timestamp.timestamp() pos = self.get_pos_from_time(timestamp) self.setCursorPosition(pos, emit_signal) def get_pos_from_time(self, timestamp): px = timestamp idx1 = (np.abs(self.xvalues[0] - px)).argmin() x1 = self.chart.series()[0].at(idx1).x() pos1 = self.chart.mapToPosition(QPointF(x1, 0)).x() idx2 = idx1 + 1 if idx2 < len(self.chart.series()[0]): x2 = self.chart.series()[0].at(idx2).x() if x2 != x1: pos2 = self.chart.mapToPosition(QPointF(x2, 0)).x() x2 /= 1000 x1 /= 1000 pos = (((px - x1) / (x2 - x1)) * (pos2 - pos1)) + pos1 else: pos = pos1 else: pos = pos1 return pos def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # Update cursor height area = self.chart.plotArea() line = self.cursor.line() self.cursor.setLine(line.x1(), area.y(), line.x2(), area.y() + area.height()) def zoom_in(self): self.chart.zoomIn() self.update_axes() def zoom_out(self): self.chart.zoomOut() self.update_axes() def zoom_area(self): if self.selection_rec: zoom_rec = self.selection_rec.rect() zoom_rec.setY(0) zoom_rec.setHeight(self.chart.plotArea().height()) self.chart.zoomIn(zoom_rec) self.clearSelectionArea(True) self.update_axes() def zoom_reset(self): self.chart.zoomReset() self.update_axes() def get_displayed_start_time(self): min_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() xmap = self.chart.mapToValue(QPointF(min_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) def get_displayed_end_time(self): max_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() max_x += self.chart.mapToScene( self.chart.plotArea()).boundingRect().width() xmap = self.chart.mapToValue(QPointF(max_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) @property def is_zoomed(self): return self.chart.isZoomed()
class TreeScene(QGraphicsScene): NODE_X_OFFSET = 100 NODE_Y_OFFSET = 100 ZOOM_SENSITIVITY = 0.05 def __init__(self, view, gui, parent=None): """ The constructor for a tree scene :param view: The view for this scene :param gui: the main window :param parent: The parent widget of this scene """ super(TreeScene, self).__init__(QRectF(0, 0, 1, 1), parent) self.gui = gui self.app = self.gui.app self.view = view # mapping for model node to view node self.nodes = {} # disconnected graphical nodes in the view self.disconnected_nodes = [] # Indicates if the scene is in info mode self.info_mode = False # Indicates if the scene is in simulator mode self.simulator_mode = False # Indicates if the TreeScene is dragged around self.dragging = False # The tree ModelNode being added to the scene self.adding_node: ModelNode = None # The drag drop node being added self.drag_drop_node: ViewNode = None # The node being connected to the tree self.connecting_node = None # Line connected to cursor when connecting nodes self.connecting_line = None # Indicates the node being dragged self.dragging_node = None # Position data for node reconnection self.reconnecting_node = None self.reconnect_edge_data = None # start position of every node self.node_init_pos = None # root of the tree self.root_ui_node = None def add_tree(self, tree: Tree, x: int = None, y: int = 0): """ Adds model tree recursively to the scene :param x: The x position for the root node :param y: The y position for the root node :param tree: Model tree """ # use the top center as default tree position if not x: x = self.width() / 2 if not y: y = -(self.view.viewport().height() / 2) + ViewNode.NODE_HEIGHT self.node_init_pos = (x, y) # remove old content self.clear() tree = deepcopy(tree) # check if there is a root, otherwise do not display the tree if not tree.root: return # add disconnected nodes to root all_children = [ c for node in tree.nodes.values() for c in node.children ] disconnected_model_nodes = [ tree.nodes[node] for node in tree.nodes if node != tree.root and node not in all_children ] # sort nodes based on number of children disconnected_model_nodes = sorted(disconnected_model_nodes, key=lambda n: len(n.children), reverse=True) for i, d_node in enumerate(disconnected_model_nodes): d_view_node = DisconnectedNode(d_node) tree.nodes[d_view_node.id] = d_view_node tree.nodes[tree.root].children.append(d_view_node.id) # start recursively drawing tree root_node = tree.nodes[tree.root] self.root_ui_node = self.add_subtree(tree, root_node)[0] self.root_ui_node.top_collapse_expand_button.hide() self.addItem(self.root_ui_node) def clear(self): self.root_ui_node = None self.nodes.clear() self.disconnected_nodes.clear() return super(TreeScene, self).clear() def keyReleaseEvent(self, key_event): if key_event.key() == Qt.Key_Escape: if self.connecting_node: self.disconnected_nodes.append(self.connecting_node) self.removeItem(self.connecting_line) self.app.restoreOverrideCursor() # remove reset cursor filter (cursor already reset) self.app.removeEventFilter(self.app.wait_for_click_filter) node = self.gui.tree.nodes.get(self.connecting_node.id) self.connecting_node = None self.gui.update_tree(node) elif self.reconnecting_node: if self.reconnecting_node not in self.disconnected_nodes: self.disconnected_nodes.append(self.reconnecting_node) self.removeItem(self.connecting_line) self.app.restoreOverrideCursor() # remove reset cursor filter (cursor already reset) self.app.removeEventFilter(self.app.wait_for_click_filter) old_parent = self.gui.tree.nodes[ self.reconnect_edge_data['old_parent'].id] self.reconnecting_node = None self.reconnect_edge_data = None self.gui.update_tree(old_parent) def add_subtree(self, tree: Tree, subtree_root: ModelNode): """ Recursive functions that adds node and its children to the tree. :param tree: The complete tree, used for node lookup :param subtree_root: The root of this subtree/branch :return: The created subtree root node, the width of both sides of the subtree """ subtree_root_node = ViewNode(*self.node_init_pos, scene=self, title=subtree_root.title, model_node=subtree_root, node_types=self.gui.load_node_types) if isinstance(subtree_root, DisconnectedNode): subtree_root_node.top_collapse_expand_button.hide() self.nodes[subtree_root.id] = subtree_root_node if subtree_root.id not in self.gui.tree.nodes: self.gui.tree.nodes[subtree_root.id] = subtree_root if subtree_root.id == tree.root: connected_children = [ c for c in subtree_root.children if not isinstance(tree.nodes[c], DisconnectedNode) ] middle_index = (len(connected_children) - 1) / 2 else: middle_index = (len(subtree_root.children) - 1) / 2 # keep track of level width to prevent overlapping nodes subtree_left_width = subtree_right_width = 0 # store the left nodes so that they can be moved left during creation left_nodes = [] # iterate over the left nodes for i, child_id in enumerate( subtree_root.children[:math.ceil(middle_index)]): child = tree.nodes[child_id] # add the child and its own subtree, # returned values are used to adjust the nodes position to prevent overlap child_view_node, child_subtree_width_left, child_subtree_width_right = self.add_subtree( tree, child) subtree_root_node.add_child(child_view_node) move_x = -(child_subtree_width_left + child_subtree_width_right) # prevent double spacing when there is no middle node if i == math.floor(middle_index) and not middle_index.is_integer(): # use half the offset because the other half is added later for the other part of the tree move_x -= self.NODE_X_OFFSET / 2 child_view_node.moveBy( -(child_subtree_width_right + (self.NODE_X_OFFSET / 2)), (subtree_root_node.rect().height() / 2) + self.NODE_Y_OFFSET) else: # use the default node offset move_x -= self.NODE_X_OFFSET child_view_node.moveBy( -(child_subtree_width_right + self.NODE_X_OFFSET), (subtree_root_node.rect().height() / 2) + self.NODE_Y_OFFSET) # add width to total left subtree width subtree_left_width += abs(move_x) # move all previous nodes to the left to make room for the new node for n in left_nodes: n.moveBy(move_x, 0) left_nodes.append(child_view_node) # add middle node if middle_index.is_integer(): child_id = subtree_root.children[int(middle_index)] child = tree.nodes[child_id] # add the child and its own subtree, # returned values are used to adjust the nodes position to prevent overlap child_view_node, child_subtree_width_left, child_subtree_width_right = self.add_subtree( tree, child) subtree_root_node.add_child(child_view_node) child_view_node.moveBy(0, (subtree_root_node.rect().height() / 2) + self.NODE_Y_OFFSET) # move all left nodes further to the left to make room for the middle node move_x = -self.NODE_X_OFFSET + child_subtree_width_left if child_subtree_width_left > self.NODE_X_OFFSET: move_x = -child_subtree_width_left for n in left_nodes: n.moveBy(move_x, 0) subtree_left_width += child_subtree_width_left subtree_right_width += child_subtree_width_right # iterate over the right nodes for i, child_id in enumerate( subtree_root.children[math.floor(middle_index) + 1:]): child = tree.nodes[child_id] # add the child and its own subtree, # returned values are used to adjust the nodes position to prevent overlap child_view_node, child_subtree_width_left, child_subtree_width_right = self.add_subtree( tree, child) if not isinstance(child, DisconnectedNode): subtree_root_node.add_child(child_view_node) else: self.disconnected_nodes.append(child_view_node) self.addItem(child_view_node) move_x = subtree_right_width + child_subtree_width_left # add width to total right subtree width subtree_right_width += child_subtree_width_left + child_subtree_width_right # prevent double spacing when there is no middle node if i == 0 and not middle_index.is_integer(): # use half the offset because the other half is added already move_x += self.NODE_X_OFFSET / 2 subtree_right_width += (self.NODE_X_OFFSET / 2) else: # use the default node offset move_x += self.NODE_X_OFFSET subtree_right_width += self.NODE_X_OFFSET # move node next to the previous node, all the way to the right child_view_node.moveBy(move_x, 0) if not isinstance(child, DisconnectedNode): child_view_node.moveBy( 0, (subtree_root_node.rect().height() / 2) + self.NODE_Y_OFFSET) # set the widths of both subtree sides to a default value if no children or a smaller child node if not subtree_root.children or subtree_left_width < subtree_root_node.rect().width() / 2 or \ subtree_right_width < subtree_root_node.rect().width() / 2: subtree_left_width = subtree_right_width = subtree_root_node.rect( ).width() / 2 return subtree_root_node, subtree_left_width, subtree_right_width def change_root(self, node_id: str): self.gui.tree.root = node_id if node_id == '': if self.root_ui_node and not self.root_ui_node.parentItem(): self.disconnected_nodes.append(self.root_ui_node) self.root_ui_node = None else: if self.root_ui_node: self.disconnected_nodes.append(self.root_ui_node) node = self.nodes[node_id] self.root_ui_node = node try: self.disconnected_nodes.remove(node) except ValueError: pass self.update() self.gui.update_tree() def update_children(self, node_ids: List[str]): for node_id in node_ids: if node_id in self.nodes: model_node = self.gui.tree.nodes[node_id] view_node = self.nodes[node_id] if self.view.parent().property_display and \ self.view.parent().property_display.node_id in [n.id for n in view_node.nodes_below()]: self.close_property_display() for c in view_node.children: c.delete_subtree(update_tree=False) for e in view_node.edges: e.setParentItem(None) self.removeItem(e) view_node.edges.clear() for c_id in model_node.children: child_model_node = self.gui.tree.nodes[c_id] child_view_node = self.add_subtree(self.gui.tree, child_model_node)[0] view_node.add_child(child_view_node) self.align_while_colliding() def align_tree(self): """ Aligns the tree currently visible in the ui """ if self.gui.tree: if self.root_ui_node: self.align_from_node(self.root_ui_node) def align_while_colliding(self, node: ViewNode = None): if not node: node = self.root_ui_node if self.gui.tree: if node: colliding_below = [ nb for nb in node.nodes_below() if [ ci for ci in nb.collidingItems() if isinstance(ci, ViewNode) ] ] colliding_below.sort(key=lambda n: len(n.nodes_below()), reverse=True) for node in colliding_below: align_node = node while [ ci for ci in node.collidingItems() if isinstance(ci, ViewNode) ]: if align_node.parentItem(): align_node = align_node.parentItem().parentItem() self.align_from_node(align_node) else: break def align_from_node(self, node: ViewNode): """ Align all the children of a node Works like add_subtree but repositions existing nodes instead of creating nodes :param node: The root of the alignment """ middle_index = (len(node.children) - 1) / 2 # keep track of level width to prevent overlapping nodes subtree_left_width = subtree_right_width = 0 # store the left nodes so that they can be moved left during repositioning left_nodes = [] # iterate over the left nodes for i, child in enumerate(node.children[:math.ceil(middle_index)]): # calculate values for position adjustment to prevent overlap child_subtree_width_left, child_subtree_width_right = self.align_from_node( child) # only align visible nodes if node.isVisible() and child.isVisible(): move_x = -(child_subtree_width_left + child_subtree_width_right) # reset node to start position child.setPos(0, (node.rect().height() / 2) + self.NODE_Y_OFFSET) # prevent double spacing when there is no middle node if i == math.floor( middle_index) and not middle_index.is_integer(): # use half the offset because the other half is added later for the other part of the tree move_x -= self.NODE_X_OFFSET / 2 child.moveBy( -(child_subtree_width_right + (self.NODE_X_OFFSET / 2)), 0) else: # use the default node offset move_x -= self.NODE_X_OFFSET child.moveBy( -(child_subtree_width_right + self.NODE_X_OFFSET), 0) # add width to total left subtree width subtree_left_width += abs(move_x) # move all previous nodes to the left to make room for the current node for n in left_nodes: n.moveBy(move_x, 0) left_nodes.append(child) # reposition middle node if middle_index.is_integer(): child = node.children[int(middle_index)] # calculate values for position adjustment to prevent overlap child_subtree_width_left, child_subtree_width_right = self.align_from_node( child) # only align visible nodes if node.isVisible() and child.isVisible(): # reset node to start position child.setPos(0, (node.rect().height() / 2) + self.NODE_Y_OFFSET) # move all left nodes further to the left to make room for the middle node move_x = -self.NODE_X_OFFSET + child_subtree_width_left if child_subtree_width_left > self.NODE_X_OFFSET: move_x = -child_subtree_width_left for n in left_nodes: n.moveBy(move_x, 0) subtree_left_width += child_subtree_width_left subtree_right_width += child_subtree_width_right # iterate over the right nodes right_nodes = node.children[math.floor(middle_index) + 1:] if node == self.root_ui_node: right_nodes.extend(self.disconnected_nodes) for i, child in enumerate(right_nodes): # calculate values for position adjustment to prevent overlap child_subtree_width_left, child_subtree_width_right = self.align_from_node( child) # only align visible nodes if node.isVisible() and child.isVisible(): move_x = subtree_right_width + child_subtree_width_left # add width to total right subtree width subtree_right_width += child_subtree_width_left + child_subtree_width_right # prevent double spacing when there is no middle node if i == 0 and not middle_index.is_integer(): # use half the offset because the other half is added already move_x += self.NODE_X_OFFSET / 2 subtree_right_width += (self.NODE_X_OFFSET / 2) else: # use the default node offset move_x += self.NODE_X_OFFSET subtree_right_width += self.NODE_X_OFFSET # reset node to start position if child not in self.disconnected_nodes: child.setPos(0, (node.rect().height() / 2) + self.NODE_Y_OFFSET) else: child.setPos(self.root_ui_node.pos()) # move node next to the previous node, all the way to the right child.moveBy(move_x, 0) # set the widths of both subtree sides to a default value if no children or a smaller child node if not node.children or subtree_left_width < node.rect().width() / 2 or \ subtree_right_width < node.rect().width() / 2: subtree_left_width = subtree_right_width = node.rect().width() / 2 return subtree_left_width, subtree_right_width def switch_info_mode(self, info_mode: bool): self.info_mode = info_mode if self.root_ui_node: self.root_ui_node.initiate_view(propagate=True) for n in self.disconnected_nodes: n.initiate_view(propagate=True) def mousePressEvent(self, m_event): """ Handles a mouse press on the scene :param m_event: The mouse press event and its details """ # hide connecting line to prevent it from being clicked if self.connecting_line: self.connecting_line.hide() item = self.itemAt(m_event.scenePos(), self.view.transform()) if self.connecting_line: self.connecting_line.show() if self.adding_node: x = int(m_event.scenePos().x()) y = int(m_event.scenePos().y()) self.start_node_addition(x, y) self.adding_node = None self.close_property_display() return elif self.connecting_node: if item and (isinstance(item, ViewNode) or (item.parentItem() and isinstance(item.parentItem(), ViewNode))): clicked_node = item if isinstance( item, ViewNode) else item.parentItem() if clicked_node == self.connecting_node: return self.finish_connect_edge(clicked_node) elif not item and m_event.button() == Qt.LeftButton: self.dragging = True self.view.setCursor(Qt.ClosedHandCursor) return elif self.reconnecting_node: if item and (isinstance(item, ViewNode) or (item.parentItem() and isinstance(item.parentItem(), ViewNode))): clicked_node = item if isinstance( item, ViewNode) else item.parentItem() if clicked_node == self.reconnecting_node: return self.finish_reconnect_edge(clicked_node) elif not item and m_event.button() == Qt.LeftButton: self.dragging = True self.view.setCursor(Qt.ClosedHandCursor) return else: if m_event.button() == Qt.LeftButton and item: if isinstance(item, CollapseExpandButton): pass elif isinstance(item, ViewNode): self.dragging_node = item item.dragging = True elif isinstance(item.parentItem(), ViewNode): self.dragging_node = item.parentItem() item.parentItem().dragging = True elif item.parentItem(): if isinstance(item.parentItem().parentItem(), ViewNode): self.dragging_node = item.parentItem().parentItem() item.parentItem().parentItem().dragging = True # Set dragging state of the scene elif m_event.button() == Qt.LeftButton: self.dragging = True self.view.setCursor(Qt.ClosedHandCursor) # Remove property display window and save changes if not item: self.close_property_display() super(TreeScene, self).mousePressEvent(m_event) def close_property_display(self): if self.view.parent().property_display: self.view.parent().property_display.setParent(None) self.view.parent().property_display.deleteLater() self.view.parent().property_display = None def mouseReleaseEvent(self, m_event): """ Handles a mouse release on the scene :param m_event: The mouse release event and its details """ super(TreeScene, self).mouseReleaseEvent(m_event) # reset dragging state of the scene and all nodes if m_event.button() == Qt.LeftButton: if self.dragging: self.dragging = False self.view.setCursor(Qt.OpenHandCursor) elif self.dragging_node: # reset node to default mode self.dragging_node.dragging = False self.dragging_node = None def mouseMoveEvent(self, m_event): """ Handles a mouse move on the scene :param m_event: The mouse move event and its details """ super(TreeScene, self).mouseMoveEvent(m_event) # pass move event to dragged node if self.drag_drop_node: # initiate connection state if tree has a root if self.gui.tree and self.gui.tree.root != '': self.connecting_node = self.drag_drop_node x, y = self.drag_drop_node.xpos(), self.drag_drop_node.ypos() self.connecting_line = QGraphicsLineItem( x, y - self.drag_drop_node.rect().height() / 2, x, y) # keep connection line on top self.connecting_line.setZValue(1) self.addItem(self.connecting_line) self.app.add_cross_cursor(self) else: # add root to model of the tree self.gui.tree.root = self.drag_drop_node.id self.root_ui_node = self.drag_drop_node node = self.gui.tree.nodes.get(self.drag_drop_node.id) self.gui.update_tree(node) self.drag_drop_node = None if self.dragging_node: self.dragging_node.mouseMoveEvent(m_event) return # adjust connection line when connecting node if self.connecting_line: line = self.connecting_line.line() line.setP2(m_event.scenePos() - QPoint(-1, 1)) self.connecting_line.setLine(line) # pass mouse move event to top item that accepts hover events item = self.itemAt(m_event.scenePos(), self.view.transform()) if item: if item.acceptHoverEvents(): item.mouseMoveEvent(m_event) return else: # look for parent that accepts hover events while item.parentItem(): item = item.parentItem() if item.acceptHoverEvents(): item.mouseMoveEvent(m_event) return # check if scene is being dragged and move all items accordingly if self.dragging: dx = m_event.scenePos().x() - m_event.lastScenePos().x() dy = m_event.scenePos().y() - m_event.lastScenePos().y() for g_item in [i for i in self.items() if not i.parentItem()]: if g_item == self.connecting_line: line = self.connecting_line.line() line.setP1(line.p1() + QPointF(dx, dy)) self.connecting_line.setLine(line) else: g_item.moveBy(dx, dy) def zoom(self, zoom_x, zoom_y): """ Zooms the view :param zoom_x: Zoom percentage for the x-axis :param zoom_y: Zoom percentage for the y-axis """ self.view.scale(zoom_x, zoom_y) def wheelEvent(self, wheel_event): """ Handles a mousewheel scroll in the scene :param wheel_event: The mousewheel event and its details """ zoom_value = 1 + (self.ZOOM_SENSITIVITY * (wheel_event.delta() / 120)) self.zoom(zoom_value, zoom_value) def dragEnterEvent(self, drag_drop_event): if self.drag_drop_node: return mime_data = drag_drop_event.mimeData() if mime_data.hasText() and self.gui.tree: drag_drop_event.accept() node_type = json.loads(mime_data.text()) node = NodeTypes.create_node_from_node_type(node_type) # setting this attribute starts node addition sequence in the scene self.gui.tree.add_node(node) node = ViewNode(*self.node_init_pos, scene=self, model_node=node, title=node.title, node_types=self.gui.load_node_types) self.drag_drop_node = node node.top_collapse_expand_button.hide() self.nodes[node.id] = node x, y = drag_drop_event.scenePos().x(), drag_drop_event.scenePos( ).y() node.moveBy(x - self.node_init_pos[0], y - self.node_init_pos[1]) self.addItem(node) else: drag_drop_event.ignore() def dragMoveEvent(self, drag_drop_event): x, y = drag_drop_event.scenePos().x(), drag_drop_event.scenePos().y() if self.drag_drop_node: self.drag_drop_node.setPos(x - self.node_init_pos[0], y - self.node_init_pos[1]) def dragLeaveEvent(self, drag_drop_event): if self.drag_drop_node: self.removeItem(self.drag_drop_node) self.gui.tree.nodes.pop(self.drag_drop_node.id, None) self.drag_drop_node = None def start_node_addition(self, x, y): """ Starts node addition sequence, spawn a node and let a connection line follow the cursor :param x: Clicked x position in the scene :param y: Clicked y position in the scene :return: """ # create subtree based on model node node = self.add_subtree(self.gui.tree, self.adding_node)[0] node.top_collapse_expand_button.hide() self.nodes[self.adding_node.id] = node # adjust to correct position node.moveBy(x - self.node_init_pos[0], y - self.node_init_pos[1]) self.addItem(node) self.gui.tree.add_node(self.adding_node) # initiate connection state if tree has a root if self.gui.tree and self.gui.tree.root != '': self.connecting_node = node self.connecting_line = QGraphicsLineItem( x, y - node.rect().height() / 2, x, y) # keep connection line on top self.connecting_line.setZValue(1) self.addItem(self.connecting_line) else: # add root to model of the tree self.gui.tree.root = node.id # reset back to normal cursor self.app.restoreOverrideCursor() def finish_connect_edge(self, parent_node): # check for cycles in subtree connecting_model_node = self.gui.tree.nodes.get( self.connecting_node.id) parent_model_node = self.gui.tree.nodes.get(parent_node.id) if TreeScene.check_for_cycles_when_connecting(connecting_model_node, parent_model_node, self.gui.tree): return # remember current node position node_pos = (self.connecting_node.xpos(), self.connecting_node.ypos()) # add child to parent ViewNode parent_node.add_child(self.connecting_node) # move node back to original position self.connecting_node.moveBy(node_pos[0] - self.connecting_node.xpos(), node_pos[1] - self.connecting_node.ypos()) # sort the children in the UI and get correct model node order sorted_children = parent_node.sort_children() # set correct child order parent_model_node.children = [c.id for c in sorted_children] self.gui.tree.nodes[parent_model_node.id].children = [ c.id for c in sorted_children ] self.removeItem(self.connecting_line) # reset back to normal cursor self.app.restoreOverrideCursor() # remove reset cursor filter (cursor already reset) self.app.removeEventFilter(self.app.wait_for_click_filter) node = self.gui.tree.nodes.get(self.connecting_node.id) self.gui.update_tree(node) self.connecting_node = None self.connecting_line = None def start_reconnect_edge(self, node): self.reconnecting_node = node self.connecting_line = QGraphicsLineItem( node.xpos(), node.ypos() - node.rect().height() / 2, node.xpos(), node.ypos()) self.connecting_line.setZValue(1) self.addItem(self.connecting_line) if node.parentItem(): self.reconnect_edge_data = node.detach_from_parent() self.gui.tree.nodes[ self.reconnect_edge_data['old_parent'].id].children.remove( node.id) node.top_collapse_expand_button.hide() self.app.add_cross_cursor(self) def finish_reconnect_edge(self, parent_node): reconnecting_model_node = self.gui.tree.nodes.get( self.reconnecting_node.id) parent_model_node = self.gui.tree.nodes.get(parent_node.id) if TreeScene.check_for_cycles_when_connecting(reconnecting_model_node, parent_model_node, self.gui.tree): return self.reconnecting_node.attach_to_parent(self.reconnect_edge_data, parent_node) self.reconnecting_node.top_collapse_expand_button.show() if self.reconnecting_node in self.disconnected_nodes: self.disconnected_nodes.remove(self.reconnecting_node) sorted_children = parent_node.sort_children() self.gui.tree.nodes[parent_node.id].children = [ c.id for c in sorted_children ] self.removeItem(self.connecting_line) # reset back to normal cursor self.app.restoreOverrideCursor() # remove reset cursor filter (cursor already reset) self.app.removeEventFilter(self.app.wait_for_click_filter) node = self.gui.tree.nodes.get(self.reconnecting_node.id) self.reconnecting_node = None self.reconnect_edge_data = None self.gui.update_tree(node) def change_colors(self, node_colors: dict): for node in self.nodes: if node in node_colors: self.nodes[node].simulator_brush.setColor(node_colors[node]) self.update() @staticmethod def check_for_cycles_when_connecting(subtree_node, parent_node: ModelNode, tree: Tree) -> bool: cycles = False cycles |= True if parent_node.id is subtree_node.id else False cycles |= True if parent_node.id in tree.nodes[ subtree_node.id].children else False for child_id in tree.nodes[subtree_node.id].children: cycles |= TreeScene.check_for_cycles_when_connecting( tree.nodes.get(child_id), parent_node, tree) return cycles
class AttackTreeScene(QGraphicsScene): """ This Class Implements the click actions for the graphics scene """ def __init__(self, parent=None): """ Constructor for the AttackTreeScene. Sets the needed class variables and initializes the context menu :param parent: Parent widget for the AttackTreeScene """ super().__init__(parent) self.startCollisions = None self.dstCollisions = None self.conjunction = None self.insertLine = None self.mousePos = (0, 0) self.menu = QMenu(parent) self.menu.addAction('Alternative', self.addAlternative) self.menu.addAction('Composition', self.addComposition) self.menu.addAction('Sequence', self.addSequence) self.menu.addAction('Threshold', self.addThreshold) def addAlternative(self): """ Adds an alternative as edge """ self.addEdge('alternative') def addComposition(self): """ Adds an composition as edge """ self.addEdge('composition') def addSequence(self): """ Adds an sequence as edge """ self.addEdge('sequence') def addThreshold(self): """ Adds an threshold as edge """ self.addEdge('threshold') def addEdge(self, type): """ Adds an edge to the graph with the specific type :param type: Type of the edge (alternative|alternative|sequence|threshold) """ node = types.Conjunction(conjunctionType=type) self.parent().tree.addNode(node) n = Conjunction(node, self.parent(), x=self.mousePos[0], y=self.mousePos[1]) self.addItem(n) self.reset() self.parent().saved = False def reset(self): """ Resets all actions if a mode was selected. Also deletes the Line for inserting a edge """ self.startCollisions = None self.dstCollisions = None self.conjunction = None self.parent().mode = 0 self.parent().modeAction.setChecked(False) self.parent().modeAction = self.parent().defaultModeAction self.parent().modeAction.setChecked(True) if self.insertLine is not None: self.removeItem(self.insertLine) self.insertLine = None self.parent().setCursor(Qt.ArrowCursor) self.parent().graphicsView.setDragMode(QGraphicsView.RubberBandDrag) def mousePressEvent(self, mouseEvent): """ Handles the press event for the mouse On click it will insert a node or set the start position for a conjunction :param mouseEvent: Mouse Event """ if mouseEvent.button() == Qt.LeftButton: if self.parent().mode == 1: """ Mode 1: Insert threat node """ self.parent().addLastAction() node = types.Threat() self.parent().tree.addNode(node) n = Threat(node, self.parent(), mouseEvent.scenePos().x(), mouseEvent.scenePos().y()) self.addItem(n) edit = NodeEdit(n, n.parent) edit.exec() self.parent().saved = False self.reset() super().mousePressEvent(mouseEvent) elif self.parent().mode == 2: """ Mode 2: Insert countermeasure node """ self.parent().addLastAction() node = types.Countermeasure() self.parent().tree.addNode(node) n = Countermeasure(node, self.parent(), mouseEvent.scenePos().x(), mouseEvent.scenePos().y()) self.addItem(n) self.parent().saved = False edit = NodeEdit(n, n.parent) edit.exec() super().mousePressEvent(mouseEvent) self.reset() elif self.parent().mode == 3: """ Mode 3: Insert conjunction node Displays an popup menu """ self.mousePos = mouseEvent.scenePos().x(), mouseEvent.scenePos().y() self.menu.popup( self.parent().mapToGlobal(self.parent().graphicsView.mapFromScene(mouseEvent.scenePos())), None) super().mousePressEvent(mouseEvent) self.reset() elif self.parent().mode == 4: """ Mode 4: Insert line Start node of the line is set here """ self.startCollisions = self.itemAt(mouseEvent.scenePos(), QTransform()) self.insertLine = QGraphicsLineItem(QLineF(mouseEvent.scenePos(), mouseEvent.scenePos())) self.insertLine.setPen(QPen(Qt.black, 2)) self.addItem(self.insertLine) elif self.parent().mode == 6: """ Mode 6: Insert copy buffer """ self.parent().insertCopyBuffer(mouseEvent.scenePos().x(), mouseEvent.scenePos().y()) self.reset() else: super().mousePressEvent(mouseEvent) elif mouseEvent != Qt.RightButton: self.reset() super().mousePressEvent(mouseEvent) def contextMenuEvent(self, event): """ Handles the event to open a context menu on a node The event will open a context menu to edit the node :param event: context menu Event """ try: if len(self.selectedItems()) > 0: menu = QMenu(self.parent()) menu.addAction('Delete', self.deleteSelected) menu.addAction('Select Children', self.selectNodesChildren) menu.addAction('Copy', self.parent().copy) menu.addAction('Cut', self.parent().cut) menu.popup(event.screenPos(), None) elif self.itemAt(event.scenePos(), QTransform()) is not None: item = self.itemAt(event.scenePos(), QTransform()) item.setSelected(True) menu = QMenu(self.parent()) if isinstance(item.parentItem(), Node): item = item.parentItem() menu.addAction('Edit', item.edit) menu.addAction('Delete', item.delete) menu.addAction('Select Children', item.selectChildren) menu.addAction('Copy', self.parent().copy) menu.addAction('Cut', self.parent().cut) elif isinstance(item.parentItem(), QGraphicsItemGroup) and isinstance(item.parentItem().parentItem(), Node): item = item.parentItem().parentItem() menu.addAction('Edit', item.edit) menu.addAction('Delete', item.delete) menu.addAction('Select Children', item.selectChildren) menu.addAction('Copy', self.parent().copy) menu.addAction('Cut', self.parent().cut) elif isinstance(item, Edge): menu.addAction('Delete', functools.partial(self.deleteEdge, item)) menu.addAction('Select Children', item.selectChildren) else: menu.popup(event.screenPos(), None) else: menu = QMenu(self.parent()) menu.addAction('Paste', functools.partial(self.parent().insertCopyBuffer, event.scenePos().x(), event.scenePos().y())) menu.popup(event.screenPos(), None) except Exception as e: print(traceback.format_exc()) def deleteEdge(self, edge): """ Deletes an edge :param edge: Edge to delete """ self.removeItem(edge) edge.start.childEdges.remove(edge) edge.dst.parentEdges.remove(edge) self.parent().tree.removeEdge(edge.start.node.id + '-' + edge.dst.node.id) def deleteSelected(self): """ Deletes an selection """ self.parent().addLastAction() deleted = [] for i in self.selectedItems(): if i not in deleted: if isinstance(i, Node): deleted.append(i) i.delete() elif isinstance(i, Edge): deleted.append(i) if not (i.start in deleted or i.dst in deleted): self.removeItem(i) i.start.childEdges.remove(i) i.dst.parentEdges.remove(i) self.parent().tree.removeEdge(i.start.node.id + '-' + i.dst.node.id) def selectNodesChildren(self): """ Selects all children of the selection """ for i in self.selectedItems(): i.selectChildren() def mouseMoveEvent(self, mouseEvent): """ Handler for the move event of the mouse. If the mode is to draw a line (3) it will update the feedback line :param mouseEvent: Mouse Event """ if self.insertLine is not None: newLine = QLineF(self.insertLine.line().p1(), mouseEvent.scenePos()) self.insertLine.setLine(newLine) super().mouseMoveEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): """ Handles the mouse release event. In this event the edge will be completed or the item to delete are certain :param mouseEvent: Mouse Event """ if mouseEvent.button() == Qt.LeftButton: if self.parent().mode == 4: """ Mode 4: Insert edge Inserts the edge """ self.parent().addLastAction() self.insertLine.setZValue(-1) self.dstCollisions = self.itemAt(mouseEvent.scenePos(), QTransform()) if self.startCollisions is None or self.dstCollisions is None \ or self.startCollisions == self.dstCollisions: self.reset() super().mouseReleaseEvent(mouseEvent) return if isinstance(self.startCollisions.parentItem(), Node): """ Gets the start node view object """ self.startCollisions = self.startCollisions.parentItem() elif isinstance(self.startCollisions.parentItem(), QGraphicsItemGroup) \ and isinstance(self.startCollisions.parentItem().parentItem(), Node): """ Gets the start node view object if the user clicks on the text in the item """ self.startCollisions = self.startCollisions.parentItem().parentItem() else: self.reset() super().mouseReleaseEvent(mouseEvent) return if isinstance(self.dstCollisions.parentItem(), Node): """ Gets the destination node view object """ self.dstCollisions = self.dstCollisions.parentItem() elif isinstance(self.dstCollisions.parentItem(), QGraphicsItemGroup) \ and isinstance(self.dstCollisions.parentItem().parentItem(), Node): """ Gets the destination node view object if the user clicks on the text in the item """ self.dstCollisions = self.dstCollisions.parentItem().parentItem() else: self.reset() super().mouseReleaseEvent(mouseEvent) return if self.parent().tree.addEdge(self.startCollisions.node.id, self.dstCollisions.node.id) is True: self.startCollisions.addEdge(self.dstCollisions) if isinstance(self.startCollisions, Conjunction): self.startCollisions.fixParentEdgeRec() self.startCollisions.redraw() self.reset() self.parent().saved = False else: MessageBox('Adding Edge is not possible', 'The Edge is not supported', icon=QMessageBox.Critical).run() self.reset() elif self.parent().mode == 5: """ Mode 4: Deletes selected items Deletes all selected items plus edges from on to the selection """ self.deleteSelected() self.reset() self.parent().saved = False else: self.reset() super().mouseReleaseEvent(mouseEvent)
class SinglePipeSegmentItem(SegmentItemBase): def __init__(self, startNode, endNode, parent: SinglePipeConnection): super().__init__(startNode, endNode, parent) self._singlePipeConnection = parent self.singleLine = QGraphicsLineItem(self) self.linearGrad = None self.initGrad() def _createSegment(self, startNode, endNode) -> "SegmentItemBase": return SinglePipeSegmentItem(startNode, endNode, self._singlePipeConnection) def _getContextMenu(self) -> QMenu: menu = super()._getContextMenu() editHydraulicLoopAction = menu.addAction("Edit hydraulic loop") editHydraulicLoopAction.triggered.connect( self._singlePipeConnection.editHydraulicLoop) return menu def _setLineImpl(self, x1, y1, x2, y2): self.initGrad() self.singleLine.setLine(x1, y1, x2, y2) self.linePoints = self.singleLine.line() def initGrad(self): color = QtCore.Qt.white pen1 = QtGui.QPen(color, 4) if isinstance(self.startNode.parent, CornerItem): startBlock = self.startNode.firstNode().parent else: startBlock = self.startNode.parent if isinstance(self.endNode.parent, CornerItem): endBlock = self.endNode.lastNode().parent else: endBlock = self.endNode.parent self.linearGrad = QLinearGradient( QPointF(startBlock.fromPort.scenePos().x(), startBlock.fromPort.scenePos().y()), QPointF(endBlock.toPort.scenePos().x(), endBlock.toPort.scenePos().y()), ) self.linearGrad.setColorAt(0, QtCore.Qt.blue) self.linearGrad.setColorAt(1, QtCore.Qt.red) self.linearGrad.setColorAt(0, QtCore.Qt.gray) self.linearGrad.setColorAt(1, QtCore.Qt.black) pen1.setBrush(QBrush(self.linearGrad)) self.singleLine.setPen(pen1) def updateGrad(self): color = QtCore.Qt.white pen1 = QtGui.QPen(color, 4) totLenConn = self.connection.totalLength() partLen1 = self.connection.partialLength(self.startNode) partLen2 = self.connection.partialLength(self.endNode) if isinstance(self.startNode.parent, CornerItem): startGradP = QPointF(self.startNode.parent.scenePos().x(), self.startNode.parent.scenePos().y()) elif self.startNode.prevN() is None: startGradP = QPointF(self.startNode.parent.fromPort.scenePos().x(), self.startNode.parent.fromPort.scenePos().y()) else: startGradP = QPointF(self.line().p1().x(), self.line().p1().y()) if isinstance(self.endNode.parent, CornerItem): endGradP = QPointF(self.endNode.parent.scenePos().x(), self.endNode.parent.scenePos().y()) elif self.endNode.nextN() is None: endGradP = QPointF(self.endNode.parent.toPort.scenePos().x(), self.endNode.parent.toPort.scenePos().y()) else: endGradP = QPointF(self.line().p2().x(), self.line().p2().y()) self.linearGrad = QLinearGradient(startGradP, endGradP) self.linearGrad.setColorAt(0, self.interpolate(partLen1, totLenConn)) self.linearGrad.setColorAt(1, self.interpolate(partLen2, totLenConn)) pen1.setBrush(QBrush(self.linearGrad)) self.singleLine.setPen(pen1) def setSelect(self, isSelected: bool) -> None: if isSelected: selectPen = self._createSelectPen() self.singleLine.setPen(selectPen) else: self.updateGrad() def setColorAndWidthAccordingToMassflow(self, color, width): pen1 = QPen(color, width) self.singleLine.setPen(pen1)