def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0, length=10.0, **kwargs): self._orientation = orientation self._value = value self._length = length self._min = 0.0 self._max = 1.0 self._line = QLineF() # type: Optional[QLineF] self._pen = QPen() super().__init__(parent, **kwargs) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPen( make_pen(brush=QColor(50, 50, 50), width=1, cosmetic=False, style=Qt.DashLine)) if self._orientation == Qt.Vertical: self.setCursor(Qt.SizeVerCursor) else: self.setCursor(Qt.SizeHorCursor)
def test_arrowannotation(self): item = ArrowItem() self.scene.addItem(item) item.setLine(QLineF(100, 100, 100, 200)) item.setLineWidth(5) item = ArrowItem() item.setLine(QLineF(150, 100, 150, 200)) item.setLineWidth(10) item.setArrowStyle(ArrowItem.Concave) self.scene.addItem(item) item = ArrowAnnotation() item.setPos(10, 10) item.setLine(QLineF(10, 10, 200, 200)) self.scene.addItem(item) item.setLineWidth(5) def advance(): clock = time.clock() * 10 item.setLineWidth(5 + math.sin(clock) * 5) item.setColor(QColor(Qt.red).lighter(100 + 30 * math.cos(clock))) self.singleShot(0, advance) advance() self.app.exec_()
def test_arrowannotation(self): item = ArrowItem() self.scene.addItem(item) item.setLine(QLineF(100, 100, 100, 200)) item.setLineWidth(5) item = ArrowItem() item.setLine(QLineF(150, 100, 150, 200)) item.setLineWidth(10) item.setArrowStyle(ArrowItem.Concave) self.scene.addItem(item) item = ArrowAnnotation() item.setPos(10, 10) item.setLine(QLineF(10, 10, 200, 200)) self.scene.addItem(item) item.setLineWidth(5) def advance(): clock = time.clock() * 10 item.setLineWidth(5 + math.sin(clock) * 5) item.setColor(QColor(Qt.red).lighter(100 + 30 * math.cos(clock))) timer = QTimer(item, interval=10) timer.timeout.connect(advance) timer.start() self.app.exec_()
def adjustGeometry(self): # type: () -> None """ Adjust the widget geometry to exactly fit the arrow inside while preserving the arrow path scene geometry. """ # local system coordinate geom = self.geometry().translated(-self.pos()) line = self.__line arrow_rect = self.__arrowItem.shape().boundingRect() if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) geom = geom.intersected(arrow_rect) diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) geom.translate(self.pos()) self.setGeometry(geom) self.setLine(line)
def update_anchors(self): points, labels = self.master.get_anchors() if points is None: return r = self.scaled_radius * np.max(np.linalg.norm(points, axis=1)) if self.anchor_items is None: self.anchor_items = [] for point, label in zip(points, labels): anchor = AnchorItem(line=QLineF(0, 0, *point)) anchor._label.setToolTip(f"<b>{label}</b>") label = label[:MAX_LABEL_LEN - 3] + "..." if len( label) > MAX_LABEL_LEN else label anchor.setText(label) anchor.setFont(self.parameter_setter.anchor_font) visible = self.always_show_axes or np.linalg.norm(point) > r anchor.setVisible(visible) anchor.setPen(pg.mkPen((100, 100, 100))) self.plot_widget.addItem(anchor) self.anchor_items.append(anchor) else: for anchor, point, label in zip(self.anchor_items, points, labels): anchor.setLine(QLineF(0, 0, *point)) visible = self.always_show_axes or np.linalg.norm(point) > r anchor.setVisible(visible) anchor.setFont(self.parameter_setter.anchor_font)
def setLine(self, line): """ Set the arrow base line (a `QLineF` in object coordinates). """ if self.__line != line: self.__line = QLineF(line) # local item coordinate system geom = self.geometry().translated(-self.pos()) if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) arrow_shape = arrow_path_concave(line, self.lineWidth()) arrow_rect = arrow_shape.boundingRect() if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) if self.__autoAdjustGeometry: # Shrink the geometry if required. geom = geom.intersected(arrow_rect) # topLeft can move changing the local coordinates. diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) self.__arrowItem.setLine(line) self.__arrowShadowBase.setLine(line) self.__line = line # parent item coordinate system geom.translate(self.pos()) self.setGeometry(geom)
def boundingRect(self) -> QRectF: if self._line is None: if self._orientation == Qt.Vertical: self._line = QLineF(0, self._value, self._length, self._value) else: self._line = QLineF(self._value, 0, self._value, self._length) r = QRectF(self._line.p1(), self._line.p2()) penw = self.pen().width() return r.adjusted(-penw, -penw, penw, penw)
def setLine(self, line): # type: (QLineF) -> None if not isinstance(line, QLineF): raise TypeError() if line != self.__line: self.__line = QLineF(line) self.__pointsLayout() self.lineChanged.emit(line)
def __activeControlMoved(self, pos): line = QLineF(self.__line) control = self.__activeControl if control.anchor() == ControlPoint.TopLeft: line.setP1(pos) elif control.anchor() == ControlPoint.BottomRight: line.setP2(pos) if self.__line != line: self.blockSignals(True) self.setLine(line) self.blockSignals(False) self.lineEdited.emit(line)
def qpainterpath_simple_split(path, t): """ Split a QPainterPath defined simple curve. The path must be either empty or composed of a single LineToElement or CurveToElement. Parameters ---------- path : QPainterPath t : float Point where to split specified as a percentage along the path Returns ------- splitpath: Tuple[QPainterPath, QPainterPath] A pair of QPainterPaths """ assert path.elementCount() > 0 el0 = path.elementAt(0) assert el0.type == QPainterPath.MoveToElement if path.elementCount() == 1: p1 = QPainterPath() p1.moveTo(el0.x, el0.y) return p1, QPainterPath(p1) el1 = path.elementAt(1) if el1.type == QPainterPath.LineToElement: pointat = path.pointAtPercent(t) l1 = QLineF(el0.x, el0.y, pointat.x(), pointat.y()) l2 = QLineF(pointat.x(), pointat.y(), el1.x, el1.y) p1 = QPainterPath() p2 = QPainterPath() p1.addLine(l1) p2.addLine(l2) return p1, p2 elif el1.type == QPainterPath.CurveToElement: c0, c1, c2, c3 = el0, el1, path.elementAt(2), path.elementAt(3) assert all(el.type == QPainterPath.CurveToDataElement for el in [c2, c3]) cp = [QPointF(el.x, el.y) for el in [c0, c1, c2, c3]] first, second = bezier_subdivide(cp, t) p1, p2 = QPainterPath(), QPainterPath() p1.moveTo(first[0]) p1.cubicTo(*first[1:]) p2.moveTo(second[0]) p2.cubicTo(*second[1:]) return p1, p2 else: assert False
def shape_line_intersection(shape, shape_pos, line): """ Return point of intersection between shape and line that is closest to line.p1(). """ intersections, point = [], QPointF() p1 = shape.pointAtPercent(0) + shape_pos r = shape.boundingRect() for t in np.linspace(0, 1.01, 50) % 1: p2 = shape.pointAtPercent(t) + shape_pos if QLineF(p1, p2).intersect(line, point) == QLineF.BoundedIntersection: intersections.append(QPointF(point)) p1 = p2 return min(intersections, key=lambda point: QLineF(line.p1(), point).length())
def _updateTextAnchors(self): n = len(self._items) items = self._items dist = 15 shape = reduce(QPainterPath.united, [item.path() for item in items]) brect = shape.boundingRect() bradius = max(brect.width() / 2, brect.height() / 2) center = self.boundingRect().center() anchors = _category_anchors(items) self._textanchors = [] for angle, anchor_h, anchor_v in anchors: line = QLineF.fromPolar(bradius, angle) ext = QLineF.fromPolar(dist, angle) line = QLineF(line.p1(), line.p2() + ext.p2()) line = line.translated(center) anchor_pos = line.p2() self._textanchors.append((anchor_pos, anchor_h, anchor_v)) for i in range(n): self._updateTextItemPos(i)
def _anchor_circle(self): # minimum visible anchor radius (radius) minradius = self.radius / 100 + 1e-5 for item in chain(self.plotdata.anchoritem, self.plotdata.items): self.viewbox.removeItem(item) self.plotdata.anchoritem = [] self.plotdata.items = [] for anchor, var in zip(self.plotdata.anchors, self.data.domain.attributes): if True or np.linalg.norm(anchor) > minradius: axitem = AnchorItem( line=QLineF(0, 0, *anchor), text=var.name, ) axitem.setVisible(np.linalg.norm(anchor) > minradius) axitem.setPen(pg.mkPen((100, 100, 100))) axitem.setArrowVisible(True) self.plotdata.anchoritem.append(axitem) self.viewbox.addItem(axitem) hidecircle = QGraphicsEllipseItem() hidecircle.setRect( QRectF(-minradius, -minradius, 2 * minradius, 2 * minradius)) _pen = QPen(Qt.lightGray, 1) _pen.setCosmetic(True) hidecircle.setPen(_pen) self.viewbox.addItem(hidecircle) self.plotdata.items.append(hidecircle) self.plotdata.hidecircle = hidecircle
def __init__(self, parent=None, line=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) if line is None: line = QLineF(0, 0, 20, 0) self.__line = line self.__color = QColor(Qt.red) self.__arrowItem = ArrowItem(self) self.__arrowItem.setLine(line) self.__arrowItem.setBrush(self.__color) self.__arrowItem.setPen(QPen(Qt.NoPen)) self.__arrowItem.setArrowStyle(ArrowItem.Concave) self.__arrowItem.setLineWidth(5) self.__shadow = QGraphicsDropShadowEffect( blurRadius=5, offset=QPointF(1.0, 2.0), ) self.__arrowItem.setGraphicsEffect(self.__shadow) self.__shadow.setEnabled(True) self.__autoAdjustGeometry = True
def add_annotation(self, scheme_annot): """ Create a new item for :class:`SchemeAnnotation` and add it to the scene. If the `scheme_annot` is already in the scene do nothing and just return its item. """ if scheme_annot in self.__item_for_annotation: # Already added return self.__item_for_annotation[scheme_annot] if isinstance(scheme_annot, scheme.SchemeTextAnnotation): item = items.TextAnnotation() x, y, w, h = scheme_annot.rect item.setPos(x, y) item.resize(w, h) item.setTextInteractionFlags(Qt.TextEditorInteraction) font = font_from_dict(scheme_annot.font, item.font()) item.setFont(font) item.setContent(scheme_annot.content, scheme_annot.content_type) scheme_annot.content_changed.connect(item.setContent) elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation): item = items.ArrowAnnotation() start, end = scheme_annot.start_pos, scheme_annot.end_pos item.setLine(QLineF(QPointF(*start), QPointF(*end))) item.setColor(QColor(scheme_annot.color)) scheme_annot.geometry_changed.connect( self.__on_scheme_annot_geometry_change) self.add_annotation_item(item) self.__item_for_annotation[scheme_annot] = item return item
def _anchor_circle(self, variables): # minimum visible anchor radius (radius) min_radius = self._get_min_radius() axisitems = [] for anchor, var in zip(self.plotdata.axes, variables[:]): axitem = AnchorItem( line=QLineF(0, 0, *anchor), text=var.name, ) axitem.setVisible(np.linalg.norm(anchor) > min_radius) axitem.setPen(pg.mkPen((100, 100, 100))) axitem.setArrowVisible(True) self.viewbox.addItem(axitem) axisitems.append(axitem) self.plotdata.axisitems = axisitems if self.placement == self.Placement.Circular: return hidecircle = QGraphicsEllipseItem() hidecircle.setRect( QRectF(-min_radius, -min_radius, 2 * min_radius, 2 * min_radius)) _pen = QPen(Qt.lightGray, 1) _pen.setCosmetic(True) hidecircle.setPen(_pen) self.viewbox.addItem(hidecircle) self.plotdata.hidecircle = hidecircle
def setLine(self, line): # type: (QLineF) -> None """Set the baseline of the arrow (:class:`QLineF`). """ if self.__line != line: self.__line = QLineF(line) self.__updateArrowPath()
def line_extended(line, distance): """ Return an QLineF extended by `distance` units in the positive direction. """ angle = line.angle() / 360 * 2 * math.pi dx, dy = unit_point(angle, r=distance) return QLineF(line.p1(), line.p2() + QPointF(dx, dy))
def __on_lineGeometryChanged(self): # Possible geometry change from out of our control, for instance # item move as a part of a selection group. if not self.control.isControlActive(): line = self.item.line() p1, p2 = map(self.item.mapToScene, (line.p1(), line.p2())) self.control.setLine(QLineF(p1, p2))
def _draw_border(point_1, point_2, border_width, parent): pen = QPen(QColor(self.border_color)) pen.setCosmetic(True) pen.setWidth(border_width) line = QGraphicsLineItem(QLineF(point_1, point_2), parent) line.setPen(pen) return line
def paint(self, painter, option, index): curr_class_dist = np.array(index.data(Qt.DisplayRole), dtype=float) curr_class_dist /= sum(curr_class_dist) painter.save() self.drawBackground(painter, option, index) rect = option.rect if sum(curr_class_dist) > 0: pw = 3 hmargin = 5 x = rect.left() + hmargin width = rect.width() - 2 * hmargin vmargin = 1 textoffset = pw + vmargin * 2 painter.save() baseline = rect.bottom() - textoffset / 2 text = str(index.data(Qt.DisplayRole)) option.displayAlignment = Qt.AlignCenter text_rect = rect.adjusted(0, 0, 0, -textoffset * 0) self.drawDisplay(painter, option, text_rect, text) painter.setRenderHint(QPainter.Antialiasing) for prop, color in zip(curr_class_dist, self.color_schema): if prop == 0: continue painter.setPen(QPen(QBrush(color), pw)) to_x = x + prop * width line = QLineF(x, baseline, to_x, baseline) painter.drawLine(line) x = to_x painter.restore() painter.restore()
def draw_axes(self): self.remove_all_axes() for i in range(len(self.attributes)): axis_id = UserAxis + i a = self.add_axis(axis_id, line=QLineF(i, 0, i, 1), arrows=AxisStart | AxisEnd, zoomable=True) a.always_horizontal_text = True a.max_text_width = 100 a.title_margin = -10 a.text_margin = 0 a.setZValue(5) self.set_axis_title(axis_id, self.domain[self.attributes[i]].name) self.set_show_axis_title(axis_id, self.show_attr_values) if self.show_attr_values: attr = self.domain[self.attributes[i]] if attr.is_continuous: self.set_axis_scale(axis_id, self.attr_values[attr][0], self.attr_values[attr][1]) elif attr.is_discrete: attribute_values = get_variable_values_sorted( self.domain[self.attributes[i]]) attr_len = len(attribute_values) values = [ float(1.0 + 2.0 * j) / float(2 * attr_len) for j in range(len(attribute_values)) ] a.set_bounds((0, 1)) self.set_axis_labels(axis_id, labels=attribute_values, values=values)
def arrow_path_concave(line, width): """ Return a :class:`QPainterPath` of a pretty looking arrow. """ path = QPainterPath() p1, p2 = line.p1(), line.p2() if p1 == p2: return path baseline = QLineF(line) # Require some minimum length. baseline.setLength(max(line.length() - width * 3, width * 3)) start, end = baseline.p1(), baseline.p2() mid = (start + end) / 2.0 normal = QLineF.fromPolar(1.0, baseline.angle() + 90).p2() path.moveTo(start) path.lineTo(start + (normal * width / 4.0)) path.quadTo(mid + (normal * width / 4.0), end + (normal * width / 1.5)) path.lineTo(end - (normal * width / 1.5)) path.quadTo(mid - (normal * width / 4.0), start - (normal * width / 4.0)) path.closeSubpath() arrow_head_len = width * 4 arrow_head_angle = 50 line_angle = line.angle() - 180 angle_1 = line_angle - arrow_head_angle / 2.0 angle_2 = line_angle + arrow_head_angle / 2.0 points = [p2, p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(), baseline.p2(), p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(), p2] poly = QPolygonF(points) path_head = QPainterPath() path_head.addPolygon(poly) path = path.united(path_head) return path
def _add_point_items(self): r = self.radius / 100 + 1e-5 for point, var in zip(self._points, self._attributes): axitem = AnchorItem(line=QLineF(0, 0, *point), text=var.name) axitem.setVisible(np.linalg.norm(point) > r) axitem.setPen(pg.mkPen((100, 100, 100))) self.plot_widget.addItem(axitem) self._point_items.append(axitem)
def update_ends(self): try: self.prepareGeometryChange() self.setLine(QLineF(self.node1.edge_out_point(self), self.node2.edge_in_point(self))) except RuntimeError: # this gets called through QTimer.singleShot # and might already be deleted by Qt pass
def angle(point1, point2): """Return the angle between the two points in range from -180 to 180. """ angle = QLineF(point1, point2).angle() if angle > 180: return angle - 360 else: return angle
def path_link_disabled(basepath): # type: (QPainterPath) -> QPainterPath """ Return a QPainterPath 'styled' to indicate a 'disabled' link. A disabled link is displayed with a single disconnection symbol in the middle (--||--) Parameters ---------- basepath : QPainterPath The base path (a simple curve spine). Returns ------- path : QPainterPath A 'styled' link path """ segmentlen = basepath.length() px = 5 if segmentlen < 10: return QPainterPath(basepath) t = (px / 2) / segmentlen p1, _ = qpainterpath_simple_split(basepath, 0.50 - t) _, p2 = qpainterpath_simple_split(basepath, 0.50 + t) angle = -basepath.angleAtPercent(0.5) + 90 angler = math.radians(angle) normal = QPointF(math.cos(angler), math.sin(angler)) end1 = p1.currentPosition() start2 = QPointF(p2.elementAt(0).x, p2.elementAt(0).y) p1.moveTo(start2.x(), start2.y()) p1.addPath(p2) def QPainterPath_addLine(path, line): # type: (QPainterPath, QLineF) -> None path.moveTo(line.p1()) path.lineTo(line.p2()) QPainterPath_addLine(p1, QLineF(end1 - normal * 3, end1 + normal * 3)) QPainterPath_addLine(p1, QLineF(start2 - normal * 3, start2 + normal * 3)) return p1
def update_anchors(self): points, labels = self.master.get_anchors() if points is None: return r = self.scaled_radius if self.anchor_items is None: self.anchor_items = [] for point, label in zip(points, labels): anchor = AnchorItem(line=QLineF(0, 0, *point), text=label) anchor.setVisible(np.linalg.norm(point) > r) anchor.setPen(pg.mkPen((100, 100, 100))) self.plot_widget.addItem(anchor) self.anchor_items.append(anchor) else: for anchor, point, label in zip(self.anchor_items, points, labels): anchor.setLine(QLineF(0, 0, *point)) anchor.setText(label) anchor.setVisible(np.linalg.norm(point) > r)
def __init__(self, x, y, parent=None, line=QLineF(), scene_size=1, text="", **kwargs): super().__init__(parent, **kwargs) self.arrows = [ pg.ArrowItem(pos=(x - scene_size * 0.07 * np.cos(np.radians(angle)), y + scene_size * 0.07 * np.sin(np.radians(angle))), parent=self, angle=angle, headLen=13, tipAngle=45, brush=pg.mkColor(128, 128, 128)) for angle in (0, 90, 180, 270)]
def paint(self, p, opt, widget): if self.xData is None or len(self.xData) == 0: return p.setRenderHint(p.Antialiasing, True) p.setCompositionMode(p.CompositionMode_SourceOver) if self.widths is None: p.setPen(self.pen) for x0, y0, x1, y1 in zip(self.xData[::2], self.yData[::2], self.xData[1::2], self.yData[1::2]): p.drawLine(QLineF(x0, y0, x1, y1)) else: pen = QPen(self.pen) for x0, y0, x1, y1, w in zip(self.xData[::2], self.yData[::2], self.xData[1::2], self.yData[1::2], self.widths): pen.setWidth(w) p.setPen(pen) p.drawLine(QLineF(x0, y0, x1, y1))
def angle(point1, point2): # type: (QPointF, QPointF) -> float """ Return the angle between the two points in range from -180 to 180. """ angle = QLineF(point1, point2).angle() if angle > 180: return angle - 360 else: return angle
def __init__(self, parent=None, **kwargs): QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setFlag(QGraphicsItem.ItemIsFocusable) self.__line = QLineF() self.__points = [ ControlPoint(self, ControlPoint.TopLeft), # TopLeft is line start ControlPoint(self, ControlPoint.BottomRight), # line end ] self.__activeControl = None if self.scene(): self.__installFilter() for p in self.__points: p.setFlag(QGraphicsItem.ItemIsFocusable) p.setFocusProxy(self)
def paint(self, painter, option, widget=None): color, _ = Edge.Color.SELECTED if self.selected else Edge.Color.DEFAULT pen = self.pen() pen.setColor(color) pen.setBrush(QBrush(color)) pen.setWidth(np.clip(2 * self.weight(), .5, 4)) painter.setPen(pen) self.setPen(pen) if self.source == self.dest: return self.paintArc(painter, option, widget) if self.source.collidesWithItem(self.dest): return have_two_edges = len([edge for edge in self.source.edges if self.source in edge and self.dest in edge and edge is not self]) source_pos = self.source.pos() dest_pos = self.dest.pos() color = self.pen().color() painter.setBrush(color) point = shape_line_intersection(self.dest.shape(), dest_pos, QLineF(source_pos, dest_pos)) line = QLineF(source_pos, point) if have_two_edges: normal = line.normalVector() normal.setLength(15) line = QLineF(normal.p2(), point) self.label.setPos(line.pointAt(.5)) self.squares.placeBelow(self.label) self.setLine(line) painter.drawLine(line) # Draw arrow head self.arrowHead.clear() for point in self._arrowhead_points(line): self.arrowHead.append(point) painter.drawPolygon(self.arrowHead)
def adjustGeometry(self): """ Adjust the widget geometry to exactly fit the arrow inside while preserving the arrow path scene geometry. """ # local system coordinate geom = self.geometry().translated(-self.pos()) line = self.__line arrow_rect = self.__arrowItem.shape().boundingRect() if geom.isNull() and not line.isNull(): geom = QRectF(0, 0, 1, 1) if not (geom.contains(arrow_rect)): geom = geom.united(arrow_rect) geom = geom.intersected(arrow_rect) diff = geom.topLeft() line = QLineF(line.p1() - diff, line.p2() - diff) geom.translate(self.pos()) self.setGeometry(geom) self.setLine(line)
def qpainterpath_simple_split(path, t): """ Split a QPainterPath defined simple curve. The path must be either empty or composed of a single LineToElement or CurveToElement. Parameters ---------- path : QPainterPath t : float Point where to split specified as a percentage along the path Returns ------- splitpath: Tuple[QPainterPath, QPainterPath] A pair of QPainterPaths """ assert path.elementCount() > 0 el0 = path.elementAt(0) assert el0.type == QPainterPath.MoveToElement if path.elementCount() == 1: p1 = QPainterPath() p1.moveTo(el0.x, el0.y) return p1, QPainterPath(p1) el1 = path.elementAt(1) if el1.type == QPainterPath.LineToElement: pointat = path.pointAtPercent(t) l1 = QLineF(el0.x, el0.y, pointat.x(), pointat.y()) l2 = QLineF(pointat.x(), pointat.y(), el1.x, el1.y) p1 = QPainterPath() p2 = QPainterPath() p1.moveTo(l1.p1()) p1.lineTo(l1.p2()) p2.moveTo(l2.p1()) p2.lineTo(l2.p2()) return p1, p2 elif el1.type == QPainterPath.CurveToElement: c0, c1, c2, c3 = el0, el1, path.elementAt(2), path.elementAt(3) assert all(el.type == QPainterPath.CurveToDataElement for el in [c2, c3]) cp = [QPointF(el.x, el.y) for el in [c0, c1, c2, c3]] first, second = bezier_subdivide(cp, t) p1, p2 = QPainterPath(), QPainterPath() p1.moveTo(first[0]) p1.cubicTo(*first[1:]) p2.moveTo(second[0]) p2.cubicTo(*second[1:]) return p1, p2 else: assert False
def __init__(self, parent=None, **kwargs): QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setFlag(QGraphicsItem.ItemIsFocusable) self.__line = QLineF() self.__points = \ [ControlPoint(self, ControlPoint.TopLeft), # TopLeft is line start ControlPoint(self, ControlPoint.BottomRight) # line end ] self.__activeControl = None if self.scene(): self.__installFilter() for p in self.__points: p.setFlag(QGraphicsItem.ItemIsFocusable) p.setFocusProxy(self)
def arrow_path_plain(line, width): """ Return an :class:`QPainterPath` of a plain looking arrow. """ path = QPainterPath() p1, p2 = line.p1(), line.p2() if p1 == p2: return path baseline = QLineF(line) # Require some minimum length. baseline.setLength(max(line.length() - width * 3, width * 3)) path.moveTo(baseline.p1()) path.lineTo(baseline.p2()) stroker = QPainterPathStroker() stroker.setWidth(width) path = stroker.createStroke(path) arrow_head_len = width * 4 arrow_head_angle = 50 line_angle = line.angle() - 180 angle_1 = line_angle - arrow_head_angle / 2.0 angle_2 = line_angle + arrow_head_angle / 2.0 points = [ p2, p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(), p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(), p2, ] poly = QPolygonF(points) path_head = QPainterPath() path_head.addPolygon(poly) path = path.united(path_head) return path
def update(self, zoom_only=False): self.update_ticks() line_color = self.plot.color(OWPalette.Axis) text_color = self.plot.color(OWPalette.Text) if not self.graph_line or not self.scene(): return self.line_item.setLine(self.graph_line) self.line_item.setPen(line_color) if self.title: self.title_item.setHtml('<b>' + self.title + '</b>') self.title_item.setDefaultTextColor(text_color) if self.title_location == AxisMiddle: title_p = 0.5 elif self.title_location == AxisEnd: title_p = 0.95 else: title_p = 0.05 title_pos = self.graph_line.pointAt(title_p) v = self.graph_line.normalVector().unitVector() dense_text = False if hasattr(self, 'title_margin'): offset = self.title_margin elif self._ticks: if self.should_be_expanded(): offset = 55 dense_text = True else: offset = 35 else: offset = 10 if self.title_above: title_pos += (v.p2() - v.p1()) * (offset + QFontMetrics(self.title_item.font()).height()) else: title_pos -= (v.p2() - v.p1()) * offset ## TODO: Move it according to self.label_pos self.title_item.setVisible(self.show_title) self.title_item.setRotation(-self.graph_line.angle()) c = self.title_item.mapToParent(self.title_item.boundingRect().center()) tl = self.title_item.mapToParent(self.title_item.boundingRect().topLeft()) self.title_item.setPos(title_pos - c + tl) ## Arrows if not zoom_only: if self.start_arrow_item: self.scene().removeItem(self.start_arrow_item) self.start_arrow_item = None if self.end_arrow_item: self.scene().removeItem(self.end_arrow_item) self.end_arrow_item = None if self.arrows & AxisStart: if not zoom_only or not self.start_arrow_item: self.start_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.start_arrow_item.setPos(self.graph_line.p1()) self.start_arrow_item.setRotation(-self.graph_line.angle() + 180) self.start_arrow_item.setBrush(line_color) self.start_arrow_item.setPen(line_color) if self.arrows & AxisEnd: if not zoom_only or not self.end_arrow_item: self.end_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.end_arrow_item.setPos(self.graph_line.p2()) self.end_arrow_item.setRotation(-self.graph_line.angle()) self.end_arrow_item.setBrush(line_color) self.end_arrow_item.setPen(line_color) ## Labels n = len(self._ticks) resize_plot_item_list(self.label_items, n, QGraphicsTextItem, self) resize_plot_item_list(self.label_bg_items, n, QGraphicsRectItem, self) resize_plot_item_list(self.tick_items, n, QGraphicsLineItem, self) test_rect = QRectF(self.graph_line.p1(), self.graph_line.p2()).normalized() test_rect.adjust(-1, -1, 1, 1) n_v = self.graph_line.normalVector().unitVector() if self.title_above: n_p = n_v.p2() - n_v.p1() else: n_p = n_v.p1() - n_v.p2() l_v = self.graph_line.unitVector() l_p = l_v.p2() - l_v.p1() for i in range(n): pos, text, size, step = self._ticks[i] hs = 0.5 * step tick_pos = self.map_to_graph(pos) if not test_rect.contains(tick_pos): self.tick_items[i].setVisible(False) self.label_items[i].setVisible(False) continue item = self.label_items[i] item.setVisible(True) if not zoom_only: if self.id in XAxes or getattr(self, 'is_horizontal', False): item.setHtml('<center>' + Qt.escape(text.strip()) + '</center>') else: item.setHtml(Qt.escape(text.strip())) item.setTextWidth(-1) text_angle = 0 if dense_text: w = min(item.boundingRect().width(), self.max_text_width) item.setTextWidth(w) if self.title_above: label_pos = tick_pos + n_p * (w + self.text_margin) + l_p * item.boundingRect().height() / 2 else: label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect().height() / 2 text_angle = -90 if self.title_above else 90 else: w = min(item.boundingRect().width(), QLineF(self.map_to_graph(pos - hs), self.map_to_graph(pos + hs)).length()) label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect().height() / 2 item.setTextWidth(w) if not self.always_horizontal_text: if self.title_above: item.setRotation(-self.graph_line.angle() - text_angle) else: item.setRotation(self.graph_line.angle() - text_angle) item.setPos(label_pos) item.setDefaultTextColor(text_color) self.label_bg_items[i].setRect(item.boundingRect()) self.label_bg_items[i].setPen(QPen(Qt.NoPen)) self.label_bg_items[i].setBrush(self.plot.color(OWPalette.Canvas)) item = self.tick_items[i] item.setVisible(True) tick_line = QLineF(v) tick_line.translate(-tick_line.p1()) tick_line.setLength(size) if self.title_above: tick_line.setAngle(tick_line.angle() + 180) item.setLine(tick_line) item.setPen(line_color) item.setPos(self.map_to_graph(pos))
class ControlPointLine(QGraphicsObject): lineChanged = Signal(QLineF) lineEdited = Signal(QLineF) def __init__(self, parent=None, **kwargs): QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setFlag(QGraphicsItem.ItemIsFocusable) self.__line = QLineF() self.__points = \ [ControlPoint(self, ControlPoint.TopLeft), # TopLeft is line start ControlPoint(self, ControlPoint.BottomRight) # line end ] self.__activeControl = None if self.scene(): self.__installFilter() for p in self.__points: p.setFlag(QGraphicsItem.ItemIsFocusable) p.setFocusProxy(self) def setLine(self, line): if not isinstance(line, QLineF): raise TypeError() if line != self.__line: self.__line = line self.__pointsLayout() self.lineChanged.emit(line) def line(self): return self.__line def isControlActive(self): """Return the state of the control. True if the control is active (user is dragging one of the points) False otherwise. """ return self.__activeControl is not None def __installFilter(self): for p in self.__points: p.installSceneEventFilter(self) def itemChange(self, change, value): if change == QGraphicsItem.ItemSceneHasChanged: if self.scene(): self.__installFilter() return QGraphicsObject.itemChange(self, change, value) def sceneEventFilter(self, obj, event): try: obj = toGraphicsObjectIfPossible(obj) if isinstance(obj, ControlPoint): etype = event.type() if etype == QEvent.GraphicsSceneMousePress: self.__setActiveControl(obj) elif etype == QEvent.GraphicsSceneMouseRelease: self.__setActiveControl(None) return QGraphicsObject.sceneEventFilter(self, obj, event) except Exception: log.error("", exc_info=True) def __pointsLayout(self): self.__points[0].setPos(self.__line.p1()) self.__points[1].setPos(self.__line.p2()) def __setActiveControl(self, control): if self.__activeControl != control: if self.__activeControl is not None: self.__activeControl.positionChanged[QPointF].disconnect( self.__activeControlMoved ) self.__activeControl = control if control is not None: control.positionChanged[QPointF].connect( self.__activeControlMoved ) def __activeControlMoved(self, pos): line = QLineF(self.__line) control = self.__activeControl if control.anchor() == ControlPoint.TopLeft: line.setP1(pos) elif control.anchor() == ControlPoint.BottomRight: line.setP2(pos) if self.__line != line: self.blockSignals(True) self.setLine(line) self.blockSignals(False) self.lineEdited.emit(line) def boundingRect(self): return QRectF()
def paintArc(self, painter, option, widget): assert self.source is self.dest node = self.source def best_angle(): """...is the one furthest away from all other angles""" angles = [QLineF(node.pos(), other.pos()).angle() for other in chain((edge.source for edge in node.edges if edge.dest == node and edge.source != node), (edge.dest for edge in node.edges if edge.dest != node and edge.source == node))] angles.sort() if not angles: # If this self-constraint is the only edge return 225 deltas = np.array(angles[1:] + [360 + angles[0]]) - angles return (angles[deltas.argmax()] + deltas.max()/2) % 360 angle = best_angle() inf = QPointF(-1e20, -1e20) # Doesn't work with real -np.inf! line0 = QLineF(node.pos(), inf) line1 = QLineF(node.pos(), inf) line2 = QLineF(node.pos(), inf) line0.setAngle(angle) line1.setAngle(angle - 13) line2.setAngle(angle + 13) p0 = shape_line_intersection(node.shape(), node.pos(), line0) p1 = shape_line_intersection(node.shape(), node.pos(), line1) p2 = shape_line_intersection(node.shape(), node.pos(), line2) path = QPainterPath() path.moveTo(p1) line = QLineF(node.pos(), p0) line.setLength(3*line.length()) pt = line.p2() path.quadTo(pt, p2) line = QLineF(node.pos(), pt) self.setLine(line) # This invalidates DeviceCoordinateCache painter.drawPath(path) # Draw arrow head line = QLineF(pt, p2) self.arrowHead.clear() for point in self._arrowhead_points(line): self.arrowHead.append(point) painter.setBrush(self.pen().color()) painter.drawPolygon(self.arrowHead) # Update label position self.label.setPos(path.pointAtPercent(.5)) if 90 < angle < 270: # Right-align the label pos = self.label.pos() x, y = pos.x(), pos.y() self.label.setPos(x - self.label.boundingRect().width(), y) self.squares.placeBelow(self.label)
def adjust(self): line = QLineF(self.source.pos(), self.dest.pos()) self.setLine(line) self.label.setPos(line.pointAt(.5) - self.label.boundingRect().center()) self.squares.placeBelow(self.label)