class PathWorkplaneOutline(QGraphicsRectItem): def __init__(self, parent=None): super(PathWorkplaneOutline, self).__init__(parent) self.setPen(getNoPen()) self._path = QGraphicsPathItem(self) self._path.setBrush(getNoBrush()) self._path.setPen(newPenObj(styles.BLUE_STROKE, 0)) # end def def updateAppearance(self): tl = self.rect().topLeft() tl1 = tl + QPointF(0, -BASE_WIDTH/2) tl2 = tl + QPointF(BASE_WIDTH/2, -BASE_WIDTH/2) bl = self.rect().bottomLeft() bl1 = bl + QPointF(0, BASE_WIDTH/2) bl2 = bl + QPointF(BASE_WIDTH/2, BASE_WIDTH/2) tr = self.rect().topRight() tr1 = tr + QPointF(0, -BASE_WIDTH/2) tr2 = tr + QPointF(-BASE_WIDTH/2, -BASE_WIDTH/2) br = self.rect().bottomRight() br1 = br + QPointF(0, BASE_WIDTH/2) br2 = br + QPointF(-BASE_WIDTH/2, BASE_WIDTH/2) pp = QPainterPath() pp.moveTo(tl2) pp.lineTo(tl1) pp.lineTo(bl1) pp.lineTo(bl2) pp.moveTo(tr2) pp.lineTo(tr1) pp.lineTo(br1) pp.lineTo(br2) self._path.setPath(pp)
def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen( QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0)
def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen(QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0)
class createDiscretionWindow(view.descritiondefinition_ui.Ui_discretionWindow, QDialog): def __init__(self, isCrack, he, parent=None): super(createDiscretionWindow, self).__init__(parent) self.highlight = QGraphicsPathItem() self.highlight.setPath(he) pen = QPen(Qt.red, 2) self.highlight.setPen(pen) self.highlight.setZValue(1) self.parent().scene.addItem(self.highlight) self.setupUi(self) self.move(1450, -1) self.isCrack = isCrack self.lineEdit_number_of_elements.setFocus(True) self.lineEdit_number_of_elements.textChanged.connect(self.change_number_of_discontinous_elements) self.radioButton_2.toggled.connect(self.setDiscontinousBoxState) if self.isCrack == True: self.checkBox_isCrack.setChecked(True) self.radioButton_2.setChecked(True) self.checkBox_isCrack.setEnabled(False) self.groupBox_type_of_discretization.setEnabled(False) def setDiscontinousBoxState(self): newState = not self.groupBox_discontinous_discretion.isEnabled() self.groupBox_discontinous_discretion.setEnabled(newState) def change_number_of_discontinous_elements(self): if self.radioButton_2.isChecked(): if self.lineEdit_number_of_elements.text() != "": if int(self.lineEdit_number_of_elements.text()) > self.verticalLayout_2.count(): for i in range(int(self.lineEdit_number_of_elements.text()) - self.verticalLayout_2.count()): lineEdit = QLineEdit() validator = QDoubleValidator() validator.setNotation(0) validator.setRange(0.1, 0.9, decimals=3) lineEdit.setValidator(validator) self.verticalLayout_2.addWidget(lineEdit) else: for i in range(self.verticalLayout_2.count()-1, int(self.lineEdit_number_of_elements.text())-1, -1): itemToDelete = self.verticalLayout_2.itemAt(i).widget() self.verticalLayout_2.removeWidget(itemToDelete) itemToDelete.deleteLater() else: pass @classmethod def getDiscretion(cls, he, isCrack=False, parent=None): dialog = cls(isCrack, he, parent) dialog.exec_() if dialog.radioButton.isChecked(): dialog.parent().scene.removeItem(dialog.highlight) if dialog.lineEdit_number_of_elements.text() != "": return int(dialog.lineEdit_number_of_elements.text()), False else: return 1, False else: spaces = [0] for i in range(dialog.verticalLayout_2.count()): spaces.append(float(dialog.verticalLayout_2.itemAt(i).widget().text()) + spaces[-1]) dialog.parent().scene.removeItem(dialog.highlight) return spaces, True
def drawGlyph(self, scene, glyph, offsetX=0, offsetY=0, color=(255, 255, 255)): path = QPainterPath() path.setFillRule(Qt.WindingFill) for c in self.decomposedPaths(glyph): segs = c.segments path.moveTo(segs[-1].points[-1].x, segs[-1].points[-1].y) for seg in segs: tuples = [(a.x, a.y) for a in seg.points] flattuples = list(sum(tuples, ())) if len(tuples) == 2: path.quadTo(*flattuples) elif len(tuples) == 3: path.cubicTo(*flattuples) else: path.lineTo(*flattuples) line = QGraphicsPathItem() line.setBrush(QColor(*color)) p = QPen() p.setStyle(Qt.NoPen) line.setPen(p) line.setPath(path) reflect = QTransform(1, 0, 0, -1, 0, 0) reflect.translate(offsetX, offsetY) # print(f"Drawing {glyph} at offset {offsetX} {offsetY}") line.setTransform(reflect) scene.addItem(line)
def drawCross(self, scene, x, y, color): path = QPainterPath() path.moveTo(x - 50, y) path.lineTo(x + 50, y) path.moveTo(x, y - 50) path.lineTo(x, y + 50) line = QGraphicsPathItem() p = QPen(QColor(*color)) p.setWidth(5) line.setPen(p) line.setPath(path) reflect = QTransform(1, 0, 0, -1, 0, 0) line.setTransform(reflect) scene.addItem(line)
def show_shape(item): """Highlight the shape of item.""" sh = QGraphicsPathItem() path = item.shape() # The shape path was returned in the item's coordinates # FIXME: translate is not enough when itemIgnoresTranformation path.translate(item.scenePos()) sh.setPath(path) pen = QPen(Qt.magenta, 1.5, Qt.DashLine) pen.setCosmetic(True) # thickness does not scale sh.setPen(pen) item.scene().addItem(sh) return sh
def updateConnector(self, item: QGraphicsPathItem, p1: QPointF, p2: QPointF, select: bool = True): path = QPainterPath(p1) path.quadTo(p1 + QPointF(-15, 0), (p1 + p2) * 0.5) path.quadTo(p2 + QPointF(15, 0), p2) if item is None: item = self.nodesScene.addPath(path) else: item.setPath(path) item.setZValue(-1) if select: item.setFlag(QGraphicsItem.ItemIsSelectable) return item
class PathWorkplaneOutline(QGraphicsRectItem): """ """ def __init__(self, parent: QGraphicsItem = None): """ Args: parent: default is ``None`` """ super(PathWorkplaneOutline, self).__init__(parent) self.setPen(getNoPen()) self._path = QGraphicsPathItem(self) self._path.setBrush(getNoBrush()) self._path.setPen(newPenObj(styles.BLUE_STROKE, 0)) # end def def updateAppearance(self): tl = self.rect().topLeft() tl1 = tl + QPointF(0, -BASE_WIDTH/2) tl2 = tl + QPointF(BASE_WIDTH/2, -BASE_WIDTH/2) bl = self.rect().bottomLeft() bl1 = bl + QPointF(0, BASE_WIDTH/2) bl2 = bl + QPointF(BASE_WIDTH/2, BASE_WIDTH/2) tr = self.rect().topRight() tr1 = tr + QPointF(0, -BASE_WIDTH/2) tr2 = tr + QPointF(-BASE_WIDTH/2, -BASE_WIDTH/2) br = self.rect().bottomRight() br1 = br + QPointF(0, BASE_WIDTH/2) br2 = br + QPointF(-BASE_WIDTH/2, BASE_WIDTH/2) pp = QPainterPath() pp.moveTo(tl2) pp.lineTo(tl1) pp.lineTo(bl1) pp.lineTo(bl2) pp.moveTo(tr2) pp.lineTo(tr1) pp.lineTo(br1) pp.lineTo(br2) self._path.setPath(pp)
class ImageViewer(QGraphicsView, QObject): def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) #self.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setDragMode(QGraphicsView.ScrollHandDrag) #self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) #self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._create_grid() self._create_grid_lines() self._pixmap = None self._selection_mode = SELECTION_MODE.NONE # polygon selection _polygon_guide_line_pen = QPen(QtGui.QColor(235, 72, 40)) _polygon_guide_line_pen.setWidth(2) _polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(_polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # rectangle selection self._box_origin = QPoint() self._box_picker = QRubberBand(QRubberBand.Rectangle, self) # free selection self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._current_label = None @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value @property def pixmap(self) -> ImagePixmap: return self._pixmap @pixmap.setter def pixmap(self, value: QPixmap): self.selection_mode = SELECTION_MODE.NONE self.resetTransform() if self.pixmap: self._scene.removeItem(self._pixmap) self.remove_annotations() self._pixmap = ImagePixmap() self._pixmap.setPixmap(value) self._pixmap.setOffset(-value.width() / 2, -value.height() / 2) self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._scene.addItem(self._pixmap) # rect=self._scene.addRect(QtCore.QRectF(0,0,100,100), QtGui.QPen(QtGui.QColor("red"))) # rect.setZValue(1.0) self.fit_to_window() @property def selection_mode(self): return self._selection_mode @selection_mode.setter def selection_mode(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._is_drawing = value == SELECTION_MODE.FREE if value == SELECTION_MODE.NONE: self.enable_items(True) else: self.enable_items(False) self._selection_mode = value def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableBox): self._scene.removeItem(item) elif isinstance(item, EditablePolygon): item.delete_polygon() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableBox): if item.label and item.label.name == label_name: self._scene.removeItem(item) elif isinstance(item, EditablePolygon): if item.label and item.label.name == label_name: item.delete_polygon() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditablePolygon) or isinstance( item, EditableBox): item.setEnabled(value) def _create_grid(self): gridSize = 15 backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) #backgroundPixmap.fill(QtGui.QColor("white")) backgroundPixmap.fill(QtGui.QColor(20, 20, 20)) #backgroundPixmap.fill(QtGui.QColor("powderblue")) painter = QtGui.QPainter(backgroundPixmap) #backgroundColor=QtGui.QColor("palegoldenrod") #backgroundColor=QtGui.QColor(237,237,237) backgroundColor = QtGui.QColor(0, 0, 0) painter.fillRect(0, 0, gridSize, gridSize, backgroundColor) painter.fillRect(gridSize, gridSize, gridSize, gridSize, backgroundColor) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def _create_grid_lines(self): pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) def fit_to_window(self): """Fit image within view.""" if not self.pixmap or not self._pixmap.pixmap(): return #self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def pixmap_hoverEnterEvent_slot(self): self.show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self.hide_guide_lines() def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: if self.selection_mode == SELECTION_MODE.BOX: if not self._box_origin.isNull(): self._box_picker.setGeometry( QRect(self._box_origin, evt.pos()).normalized()) elif self.selection_mode == SELECTION_MODE.POLYGON: if self._current_polygon: if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.selection_mode == SELECTION_MODE.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path: painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.buttons() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: self.setDragMode(QGraphicsView.NoDrag) self._box_origin = evt.pos() self._box_picker.setGeometry(QRect(self._box_origin, QSize())) self._box_picker.show() elif self._selection_mode == SELECTION_MODE.POLYGON: pixmap_rect: QRectF = self._pixmap.boundingRect() new_point = self.mapToScene(evt.pos()) # consider only the points intothe image if pixmap_rect.contains(new_point): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(new_point) else: self._current_polygon.addPoint(new_point) elif self._selection_mode == SELECTION_MODE.FREE: # start drawing new_point = self.mapToScene(evt.pos()) pixmap_rect: QRectF = self._pixmap.boundingRect() # consider only the points intothe image if pixmap_rect.contains(new_point): self.setDragMode(QGraphicsView.NoDrag) pen = QPen(QtGui.QColor(235, 72, 40)) pen.setWidth(10) self._last_point_drawn = new_point self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: roi: QRect = self._box_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() pixmap_rect = self._pixmap.boundingRect() self._box_picker.hide() if pixmap_rect == roi.united(pixmap_rect): rect = EditableBox(roi) rect.label = self.current_label self._scene.addItem(rect) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.selection_mode == SELECTION_MODE.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() pixmap_rect = self._pixmap.boundingRect() if pixmap_rect == path_rect.united(pixmap_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: if self._current_polygon and event.key() == QtCore.Qt.Key_Space: points = self._current_polygon.points self._current_polygon.label = self.current_label self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).keyPressEvent(event) def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide()
class ForcedXoverNode3(QGraphicsRectItem): """ This is a QGraphicsRectItem to allow actions and also a QGraphicsSimpleTextItem to allow a label to be drawn """ def __init__(self, virtual_helix_item, xover_item, strand3p, idx): super(ForcedXoverNode3, self).__init__(virtual_helix_item) self._vhi = virtual_helix_item self._xover_item = xover_item self._idx = idx self._is_on_top = virtual_helix_item.isStrandOnTop(strand3p) self._is_drawn_5_to_3 = strand3p.strandSet().isDrawn5to3() self._strand_type = strand3p.strandSet().strandType() self._partner_virtual_helix = virtual_helix_item self._blank_thing = QGraphicsRectItem(_blankRect, self) self._blank_thing.setBrush(QBrush(Qt.white)) self._path_thing = QGraphicsPathItem(self) self.configurePath() self.setPen(_NO_PEN) self._label = None self.setPen(_NO_PEN) self.setBrush(_NO_BRUSH) self.setRect(_rect) self.setZValue(styles.ZENDPOINTITEM + 1) # end def def updateForFloatFromVHI(self, virtual_helix_item, strand_type, idx_x, idx_y): """ """ self._vhi = virtual_helix_item self.setParentItem(virtual_helix_item) self._strand_type = strand_type self._idx = idx_x self._is_on_top = self._is_drawn_5_to_3 = True if idx_y == 0 else False self.updatePositionAndAppearance(is_from_strand=False) # end def def updateForFloatFromStrand(self, virtual_helix_item, strand3p, idx): """ """ self._vhi = virtual_helix_item self._strand = strand3p self.setParentItem(virtual_helix_item) self._idx = idx self._is_on_top = virtual_helix_item.isStrandOnTop(strand3p) self._is_drawn_5_to_3 = strand3p.strandSet().isDrawn5to3() self._strand_type = strand3p.strandSet().strandType() self.updatePositionAndAppearance() # end def def strandType(self): return self._strand_type # end def def configurePath(self): self._path_thing.setBrush(QBrush(styles.RED_STROKE)) path = PPR3 if self._is_drawn_5_to_3 else PPL3 offset = -_BASE_WIDTH if self._is_drawn_5_to_3 else _BASE_WIDTH self._path_thing.setPath(path) self._path_thing.setPos(offset, 0) offset = -_BASE_WIDTH if self._is_drawn_5_to_3 else 0 self._blank_thing.setPos(offset, 0) self._blank_thing.show() self._path_thing.show() # end def def refreshXover(self): self._xover_item.refreshXover() # end def def setPartnerVirtualHelix(self, virtual_helix_item): self._partner_virtual_helix = virtual_helix_item # end def def idx(self): return self._idx # end def def virtualHelixItem(self): return self._vhi # end def def point(self): return self._vhi.upperLeftCornerOfBaseType(self._idx, self._strand_type) # end def def floatPoint(self): pt = self.pos() return pt.x(), pt.y() # end def def isOnTop(self): return self._is_on_top # end def def isDrawn5to3(self): return self._is_drawn_5_to_3 # end def def updatePositionAndAppearance(self, is_from_strand=True): """ Sets position by asking the VirtualHelixItem Sets appearance by choosing among pre-defined painterpaths (from normalstrandgraphicsitem) depending on drawing direction. """ self.setPos(*self.point()) n5 = self._xover_item._node5 if is_from_strand: from_strand, from_idx = (n5._strand, n5._idx) if n5 != self else (None, None) if self._strand.canInstallXoverAt(self._idx, from_strand, from_idx): self.configurePath() # We can only expose a 5' end. But on which side? is_left = True if self._is_drawn_5_to_3 else False self._updateLabel(is_left) else: self.hideItems() else: self.hideItems() # end def def updateConnectivity(self): is_left = True if self._is_drawn_5_to_3 else False self._updateLabel(is_left) # end def def remove(self): """ Clean up this joint """ scene = self.scene() scene.removeItem(self._label) self._label = None scene.removeItem(self._path_thing) self._path_thing = None scene.removeItem(self._blank_thing) self._blank_thing = None scene.removeItem(self) # end def def _updateLabel(self, is_left): """ Called by updatePositionAndAppearance during init, or later by updateConnectivity. Updates drawing and position of the label. """ lbl = self._label if self._idx != None: bw = _BASE_WIDTH num = self._partner_virtual_helix.number() tBR = _FM.tightBoundingRect(str(num)) half_label_h = tBR.height()/2.0 half_label_w = tBR.width()/2.0 # determine x and y positions label_x = bw/2.0 - half_label_w if self._is_on_top: label_y = -0.25*half_label_h - 0.5 - 0.5*bw else: label_y = 2*half_label_h + 0.5 + 0.5*bw # adjust x for left vs right label_x_offset = 0.25*bw if is_left else -0.25*bw label_x += label_x_offset # adjust x for numeral 1 if num == 1: label_x -= half_label_w/2.0 # create text item if lbl == None: lbl = QGraphicsSimpleTextItem(str(num), self) lbl.setPos(label_x, label_y) lbl.setBrush(_ENAB_BRUSH) lbl.setFont(_TO_HELIX_NUM_FONT) self._label = lbl lbl.setText( str(self._partner_virtual_helix.number()) ) lbl.show() # end if # end def def hideItems(self): if self._label: self._label.hide() if self._blank_thing: self._path_thing.hide() if self._blank_thing: self._blank_thing.hide()
class EdgeItem(GraphItem): _qt_pen_styles = { 'dashed': Qt.DashLine, 'dotted': Qt.DotLine, 'solid': Qt.SolidLine, } def __init__(self, highlight_level, spline, label_center, label, from_node, to_node, parent=None, penwidth=1, edge_color=None, style='solid'): super(EdgeItem, self).__init__(highlight_level, parent) self.from_node = from_node self.from_node.add_outgoing_edge(self) self.to_node = to_node self.to_node.add_incoming_edge(self) self._default_edge_color = self._COLOR_BLACK if edge_color is not None: self._default_edge_color = edge_color self._default_text_color = self._COLOR_BLACK self._default_color = self._COLOR_BLACK self._text_brush = QBrush(self._default_color) self._shape_brush = QBrush(self._default_color) if style in ['dashed', 'dotted']: self._shape_brush = QBrush(Qt.transparent) self._label_pen = QPen() self._label_pen.setColor(self._default_text_color) self._label_pen.setJoinStyle(Qt.RoundJoin) self._edge_pen = QPen(self._label_pen) self._edge_pen.setWidth(penwidth) self._edge_pen.setColor(self._default_edge_color) self._edge_pen.setStyle(self._qt_pen_styles.get(style, Qt.SolidLine)) self._sibling_edges = set() self._label = None if label is not None: self._label = QGraphicsSimpleTextItem(label) self._label.setFont(GraphItem._LABEL_FONT) label_rect = self._label.boundingRect() label_rect.moveCenter(label_center) self._label.setPos(label_rect.x(), label_rect.y()) self._label.hoverEnterEvent = self._handle_hoverEnterEvent self._label.hoverLeaveEvent = self._handle_hoverLeaveEvent self._label.setAcceptHoverEvents(True) # spline specification according to # http://www.graphviz.org/doc/info/attrs.html#k:splineType coordinates = spline.split(' ') # extract optional end_point end_point = None if (coordinates[0].startswith('e,')): parts = coordinates.pop(0)[2:].split(',') end_point = QPointF(float(parts[0]), -float(parts[1])) # extract optional start_point if (coordinates[0].startswith('s,')): parts = coordinates.pop(0).split(',') # first point parts = coordinates.pop(0).split(',') point = QPointF(float(parts[0]), -float(parts[1])) path = QPainterPath(point) while len(coordinates) > 2: # extract triple of points for a cubic spline parts = coordinates.pop(0).split(',') point1 = QPointF(float(parts[0]), -float(parts[1])) parts = coordinates.pop(0).split(',') point2 = QPointF(float(parts[0]), -float(parts[1])) parts = coordinates.pop(0).split(',') point3 = QPointF(float(parts[0]), -float(parts[1])) path.cubicTo(point1, point2, point3) self._arrow = None if end_point is not None: # draw arrow self._arrow = QGraphicsPolygonItem() polygon = QPolygonF() polygon.append(point3) offset = QPointF(end_point - point3) corner1 = QPointF(-offset.y(), offset.x()) * 0.35 corner2 = QPointF(offset.y(), -offset.x()) * 0.35 polygon.append(point3 + corner1) polygon.append(end_point) polygon.append(point3 + corner2) self._arrow.setPolygon(polygon) self._arrow.hoverEnterEvent = self._handle_hoverEnterEvent self._arrow.hoverLeaveEvent = self._handle_hoverLeaveEvent self._arrow.setAcceptHoverEvents(True) self._path = QGraphicsPathItem(parent) self._path.setPath(path) self.addToGroup(self._path) self.set_node_color() self.set_label_color() def add_to_scene(self, scene): scene.addItem(self) if self._label is not None: scene.addItem(self._label) if self._arrow is not None: scene.addItem(self._arrow) def setToolTip(self, tool_tip): super(EdgeItem, self).setToolTip(tool_tip) if self._label is not None: self._label.setToolTip(tool_tip) if self._arrow is not None: self._arrow.setToolTip(tool_tip) def add_sibling_edge(self, edge): self._sibling_edges.add(edge) def set_node_color(self, color=None): if color is None: self._label_pen.setColor(self._default_text_color) self._text_brush.setColor(self._default_color) if self._shape_brush.isOpaque(): self._shape_brush.setColor(self._default_edge_color) self._edge_pen.setColor(self._default_edge_color) else: self._label_pen.setColor(color) self._text_brush.setColor(color) if self._shape_brush.isOpaque(): self._shape_brush.setColor(color) self._edge_pen.setColor(color) self._path.setPen(self._edge_pen) if self._arrow is not None: self._arrow.setBrush(self._shape_brush) self._arrow.setPen(self._edge_pen) def set_label_color(self, color=None): if color is None: self._label_pen.setColor(self._default_text_color) else: self._label_pen.setColor(color) if self._label is not None: self._label.setBrush(self._text_brush) self._label.setPen(self._label_pen) def _handle_hoverEnterEvent(self, event): # hovered edge item in red self.set_node_color(self._COLOR_RED) self.set_label_color(self._COLOR_RED) if self._highlight_level > 1: if self.from_node != self.to_node: # from-node in blue self.from_node.set_node_color(self._COLOR_BLUE) # to-node in green self.to_node.set_node_color(self._COLOR_GREEN) else: # from-node/in-node in teal self.from_node.set_node_color(self._COLOR_TEAL) self.to_node.set_node_color(self._COLOR_TEAL) if self._highlight_level > 2: # sibling edges in orange for sibling_edge in self._sibling_edges: sibling_edge.set_node_color(self._COLOR_ORANGE) def _handle_hoverLeaveEvent(self, event): self.set_node_color() self.set_label_color() if self._highlight_level > 1: self.from_node.set_node_color() self.to_node.set_node_color() if self._highlight_level > 2: for sibling_edge in self._sibling_edges: sibling_edge.set_node_color()
class GridItem(QGraphicsRectItem): def __init__(self, part_item: SliceNucleicAcidPartItemT, grid_type: EnumType): """previous_grid_bounds (tuple): a tuple corresponding to the bounds of the grid. Args: part_item: Description grid_type: Description """ super(GridItem, self).__init__(parent=part_item) self.setFlag(QGraphicsItem.ItemClipsChildrenToShape) self._path = None self.part_item = part_item self._path = QGraphicsPathItem(self) self.dots = (styles.DOT_SIZE, styles.DOT_SIZE / 2) # self.allow_snap = part_item.window().action_vhelix_snap.isChecked() self._draw_gridpoint_coordinates = False self.draw_lines = False self.points = [] self.points_dict = dict() self.previous_grid_bounds = None self.bounds = None self.grid_type = None self.setPen(getPenObj(styles.GRAY_STROKE, styles.EMPTY_HELIX_STROKE_WIDTH)) self.setGridType(grid_type) self.previous_grid_type = grid_type # end def def destroyItem(self): print("destroying sliceView GridItem") scene = self.scene() for point in self.points: point.destroyItem() self.points = None scene.removeItem(self) # end def def updateGrid(self): """Recreates the grid according to the latest part_item outline rect. """ part_item = self.part_item part = part_item.part() radius = part.radius() self.bounds = part_item.bounds() self.removePoints() self.setRect(self.part_item.outline.rect()) if self.grid_type == GridEnum.HONEYCOMB: self.createHoneycombGrid(part_item, radius, self.bounds) elif self.grid_type == GridEnum.SQUARE: self.createSquareGrid(part_item, radius, self.bounds) else: self._path.setPath(QPainterPath()) # end def def setGridType(self, grid_type: EnumType): """Sets the grid type. See cadnano.cnenum.GridEnum. Args: grid_type: NONE, HONEYCOMB, or SQUARE """ self.grid_type = grid_type self.updateGrid() # end def def createHoneycombGrid(self, part_item: SliceNucleicAcidPartItemT, radius: float, bounds: RectT): """Instantiate an area of griditems arranged on a honeycomb lattice. Args: part_item: Description radius: Description bounds: Description """ doLattice = HoneycombDnaPart.latticeCoordToModelXY doPosition = HoneycombDnaPart.positionModelToLatticeCoord isEven = HoneycombDnaPart.isEvenParity x_l, x_h, y_l, y_h = bounds x_l = x_l + HoneycombDnaPart.PAD_GRID_XL x_h = x_h + HoneycombDnaPart.PAD_GRID_XH y_h = y_h + HoneycombDnaPart.PAD_GRID_YL y_l = y_l + HoneycombDnaPart.PAD_GRID_YH dot_size, half_dot_size = self.dots sf = part_item.scale_factor points = self.points row_l, col_l = doPosition(radius, x_l, -y_l, scale_factor=sf) row_h, col_h = doPosition(radius, x_h, -y_h, scale_factor=sf) redo_neighbors = (row_l, col_l, row_h, col_h) != self.previous_grid_bounds or\ self.previous_grid_type != self.grid_type self.previous_grid_type = self.grid_type path = QPainterPath() is_pen_down = False draw_lines = self.draw_lines if redo_neighbors: self.points_dict = dict() for row in range(row_l, row_h): for column in range(col_l, col_h+1): x, y = doLattice(radius, row, column, scale_factor=sf) if draw_lines: if is_pen_down: path.lineTo(x, -y) else: is_pen_down = True path.moveTo(x, -y) """ +x is Left and +y is down origin of ellipse is Top Left corner so we subtract half in X and subtract in y """ pt = GridPoint(x - half_dot_size, -y - half_dot_size, dot_size, self, coord=(row, column)) if self._draw_gridpoint_coordinates: font = QFont(styles.THE_FONT, 6) path.addText(x - 5, -y + 4, font, "%s,%s" % (-row, column)) pt.setPen(getPenObj(styles.GRAY_STROKE, styles.EMPTY_HELIX_STROKE_WIDTH)) # if x == 0 and y == 0: # pt.setBrush(getBrushObj(Qt.gray)) points.append(pt) self.points_dict[(-row, column)] = pt if redo_neighbors: self.previous_grid_bounds = (row_l, col_l, row_h, col_h) is_pen_down = False if draw_lines: for column in range(col_l, col_h+1): for row in range(row_l, row_h): x, y = doLattice(radius, row, column, scale_factor=sf) if is_pen_down and isEven(row, column): path.lineTo(x, -y) is_pen_down = False else: is_pen_down = True path.moveTo(x, -y) is_pen_down = False # end for j self._path.setPath(path) # end def def createSquareGrid(self, part_item: SliceNucleicAcidPartItemT, radius: float, bounds: RectT): """Instantiate an area of griditems arranged on a square lattice. Args: part_item: Description radius: Description bounds: Description """ doLattice = SquareDnaPart.latticeCoordToModelXY doPosition = SquareDnaPart.positionToLatticeCoordRound x_l, x_h, y_l, y_h = bounds x_l = x_l + SquareDnaPart.PAD_GRID_XL x_h = x_h + SquareDnaPart.PAD_GRID_XH y_h = y_h + SquareDnaPart.PAD_GRID_YL y_l = y_l + SquareDnaPart.PAD_GRID_YH dot_size, half_dot_size = self.dots sf = part_item.scale_factor points = self.points row_l, col_l = doPosition(radius, x_l, -y_l, scale_factor=sf) row_h, col_h = doPosition(radius, x_h, -y_h, scale_factor=sf) redo_neighbors = (row_l, col_l, row_h, col_h) != \ self.previous_grid_bounds or self.previous_grid_type != self.grid_type self.previous_grid_type = self.grid_type if redo_neighbors: neighbor_map = dict() path = QPainterPath() is_pen_down = False draw_lines = self.draw_lines for row in range(row_l, row_h + 1): for column in range(col_l, col_h + 1): x, y = doLattice(radius, row, column, scale_factor=sf) if draw_lines: if is_pen_down: path.lineTo(x, -y) else: is_pen_down = True path.moveTo(x, -y) """ +x is Left and +y is down origin of ellipse is Top Left corner so we subtract half in X and subtract in y """ pt = GridPoint(x - half_dot_size, -y - half_dot_size, dot_size, self, coord=(row, column)) if self._draw_gridpoint_coordinates: font = QFont(styles.THE_FONT) path.addText(x - 10, -y + 5, font, "%s,%s" % (-row, column)) pt.setPen(getPenObj(styles.GRAY_STROKE, styles.EMPTY_HELIX_STROKE_WIDTH)) # if x == 0 and y == 0: # pt.setBrush(getBrushObj(Qt.gray)) points.append(pt) self.points_dict[(-row, column)] = pt if redo_neighbors: self.previous_grid_bounds = (row_l, col_l, row_h, col_h) is_pen_down = False # pen up # DO VERTICAL LINES if draw_lines: for column in range(col_l, col_h + 1): for row in range(row_l, row_h + 1): x, y = doLattice(radius, row, column, scale_factor=sf) if is_pen_down: path.lineTo(x, -y) else: is_pen_down = True path.moveTo(x, -y) is_pen_down = False # pen up self._path.setPath(path) # end def def removePoints(self): """Remove all points from the grid. """ points = self.points scene = self.scene() while points: scene.removeItem(points.pop()) self.points_dict = dict() # end def def showCreateHint(self, coord: Tuple[int, int], next_idnums: Tuple[int, int] = (0, 1), show_hint: bool = True, color: str = None) -> Optional[bool]: point_item = self.points_dict.get(coord) if point_item is None: return if not show_hint: point_item.showCreateHint(show_hint=False) if point_item: row, column = coord if self.grid_type is GridEnum.HONEYCOMB: parity = 0 if HoneycombDnaPart.isEvenParity(row=row, column=column) else 1 elif self.grid_type is GridEnum.SQUARE: parity = 0 if SquareDnaPart.isEvenParity(row=row, column=column) else 1 else: return id_num = next_idnums[1] if parity else next_idnums[0] point_item.showCreateHint(id_num=id_num, show_hint=show_hint, color=color) return parity == 1 # end def def setPath(self, path: QPainterPath): assert isinstance(path, QPainterPath) self._path = path # end def def path(self) -> QPainterPath: return self._path # end def def highlightGridPoint(self, row: int, column: int, on: bool = True): grid_point = self.points_dict.get((row, column)) if grid_point: grid_point.highlightGridPoint(on)
def setPath(self, path): self.__shape = None QGraphicsPathItem.setPath(self, path)
class ImageViewer(QGraphicsView, QObject): points_selection_sgn = pyqtSignal(list) key_press_sgn = pyqtSignal(QtGui.QKeyEvent) def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setDragMode(QGraphicsView.ScrollHandDrag) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._image = None self._image_original = None self._pixmap = None self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._create_grid() self._channels = [] self._current_tool = SELECTION_TOOL.POINTER self._dataset = None # create grid lines pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) self._current_label = None # rectangle selection tool self._rectangle_tool_origin = QPoint() self._rectangle_tool_picker = QRubberBand(QRubberBand.Rectangle, self) # polygon selection tool app = QApplication.instance() color = app.palette().color(QPalette.Highlight) self._polygon_guide_line_pen = QPen(color) self._polygon_guide_line_pen.setWidth(3) self._polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # circle self._current_ellipse = None # free selection tool self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._last_click_point = None self._free_Path_pen = QPen(color) self._free_Path_pen.setWidth(10) self._extreme_points = Queue(maxsize=4) @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value if self._current_label: color = QColor(self._current_label.color) self._free_Path_pen.setColor(color) self._polygon_guide_line_pen.setColor(color) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) @property def dataset(self): return self._dataset @dataset.setter def dataset(self, value): self._dataset = value @property def img_contrast(self): return self._img_contrast @img_contrast.setter def img_contrast(self, value): self._img_contrast = value @property def img_gamma(self): return self._img_gamma @img_gamma.setter def img_gamma(self, value): self._img_gamma = value @property def img_brightness(self): return self._img_brightness @img_brightness.setter def img_brightness(self, value): self._img_brightness = value @property def image(self): return self._image @image.setter def image(self, value): self._image = value self._image_original = value.copy() self.update_viewer() @property def pixmap(self) -> ImagePixmap: return self._pixmap @gui_exception def update_viewer(self, fit_image=True): rgb = cv2.cvtColor(self._image, cv2.COLOR_BGR2RGB) rgb = ImageUtilities.adjust_image(rgb, self._img_contrast, self._img_brightness) rgb = ImageUtilities.adjust_gamma(rgb, self._img_gamma) pil_image = Image.fromarray(rgb) qppixmap_image = pil_image.toqpixmap() x, y = -qppixmap_image.width() / 2, -qppixmap_image.height() / 2 if self._pixmap: self._pixmap.resetTransform() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) else: self._pixmap = ImagePixmap() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) self._scene.addItem(self._pixmap) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._hide_guide_lines() if fit_image: self.fit_to_window() @gui_exception def reset_viewer(self): self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._image = self._image_original.copy() @gui_exception def equalize_histogram(self): self._image = ImageUtilities.histogram_equalization(self._image) @gui_exception def correct_lightness(self): self._image = ImageUtilities.correct_lightness(self._image) def clusterize(self, k): self._image = ImageUtilities.kmeans(self._image.copy(), k) @property def current_tool(self): return self._current_tool @current_tool.setter def current_tool(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._current_ellipse = None self._is_drawing = value == SELECTION_TOOL.FREE self._current_tool = value self.clear_extreme_points() if value == SELECTION_TOOL.POINTER: self.enable_items(True) else: self.enable_items(False) def fit_to_window(self): if not self._pixmap or not self._pixmap.pixmap(): return self.resetTransform() self.setTransform(QtGui.QTransform()) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def _create_grid(self, gridSize=15): app: QApplication = QApplication.instance() curr_theme = "dark" if app: curr_theme = app.property("theme") if curr_theme == "light": color1 = QtGui.QColor("white") color2 = QtGui.QColor(237, 237, 237) else: color1 = QtGui.QColor(20, 20, 20) color2 = QtGui.QColor(0, 0, 0) backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) backgroundPixmap.fill(color1) painter = QtGui.QPainter(backgroundPixmap) painter.fillRect(0, 0, gridSize, gridSize, color2) painter.fillRect(gridSize, gridSize, gridSize, gridSize, color2) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) @gui_exception def keyPressEvent(self, event: QKeyEvent): if event.key() == QtCore.Qt.Key_Space: image_rect: QRectF = self._pixmap.sceneBoundingRect() if self.current_tool == SELECTION_TOOL.POLYGON and self._current_polygon: points = self._current_polygon.points self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) if len(points) <= 2: self._current_polygon.delete_item() self.current_tool = SELECTION_TOOL.POINTER elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS and \ self._extreme_points.full(): points = [] image_offset = QPointF(image_rect.width() / 2, image_rect.height() / 2) for pt in self._extreme_points.queue: pt: EditablePolygonPoint center = pt.sceneBoundingRect().center() x = math.floor(center.x() + image_offset.x()) y = math.floor(center.y() + image_offset.y()) points.append([x, y]) self.points_selection_sgn.emit(points) self.current_tool = SELECTION_TOOL.POINTER else: event.ignore() # guide lines events def _show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def _hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def _update_guide_lines(self, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): self._update_guide_lines(x, y) def pixmap_hoverEnterEvent_slot(self): self._show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self._hide_guide_lines() def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.current_tool = SELECTION_TOOL.POINTER self._polygon_guide_line.hide() @gui_exception def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() mouse_pos = self.mapToScene(evt.pos()) if evt.buttons() == QtCore.Qt.LeftButton: if self.current_tool == SELECTION_TOOL.BOX: # create rectangle self.setDragMode(QGraphicsView.NoDrag) self._rectangle_tool_origin = evt.pos() geometry = QRect(self._rectangle_tool_origin, QSize()) self._rectangle_tool_picker.setGeometry(geometry) self._rectangle_tool_picker.show() elif self.current_tool == SELECTION_TOOL.POLYGON: if image_rect.contains(mouse_pos): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.label = self._current_label self._current_polygon.tag = self._dataset self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(mouse_pos) else: self._current_polygon.addPoint(mouse_pos) elif self.current_tool == SELECTION_TOOL.ELLIPSE: if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) ellipse_rec = QtCore.QRectF(mouse_pos.x(), mouse_pos.y(), 0, 0) self._current_ellipse = EditableEllipse() self._current_ellipse.tag = self.dataset self._current_ellipse.label = self._current_label self._current_ellipse.setRect(ellipse_rec) self._scene.addItem(self._current_ellipse) elif self.current_tool == SELECTION_TOOL.FREE: # consider only the points into the image if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) self._last_point_drawn = mouse_pos self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(self._free_Path_pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS: if image_rect.contains(mouse_pos): if not self._extreme_points.full(): def delete_point(idx): del self._extreme_points.queue[idx] idx = self._extreme_points.qsize() editable_pt = EditablePolygonPoint(idx) editable_pt.signals.deleted.connect(delete_point) editable_pt.setPos(mouse_pos) self._scene.addItem(editable_pt) self._extreme_points.put(editable_pt) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) @gui_exception def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: mouse_pos = self.mapToScene(evt.pos()) image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: if not self._rectangle_tool_origin.isNull(): geometry = QRect(self._rectangle_tool_origin, evt.pos()).normalized() self._rectangle_tool_picker.setGeometry(geometry) elif self.current_tool == SELECTION_TOOL.POLYGON: if self._current_polygon and image_rect.contains(mouse_pos): if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.current_tool == SELECTION_TOOL.ELLIPSE: if self._current_ellipse and image_rect.contains(mouse_pos): ellipse_rect = self._current_ellipse.rect() ellipse_pos = QPointF(ellipse_rect.x(), ellipse_rect.y()) distance = math.hypot(mouse_pos.x() - ellipse_pos.x(), mouse_pos.y() - ellipse_pos.y()) ellipse_rect.setWidth(distance) ellipse_rect.setHeight(distance) self._current_ellipse.setRect(ellipse_rect) elif self.current_tool == SELECTION_TOOL.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path and image_rect.contains(mouse_pos): painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) @gui_exception def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: roi: QRect = self._rectangle_tool_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() self._rectangle_tool_picker.hide() if image_rect == roi.united(image_rect): rect = EditableBox(roi) rect.label = self.current_label rect.tag = self._dataset self._scene.addItem(rect) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.current_tool == SELECTION_TOOL.ELLIPSE and self._current_ellipse: roi: QRect = self._current_ellipse.boundingRect() if image_rect == roi.united(image_rect): self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) else: self._current_ellipse.delete_item() elif self.current_tool == SELECTION_TOOL.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() if image_rect == path_rect.united(image_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.tag = self.dataset path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableItem): item.delete_item() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableItem): if item.label and item.label.name == label_name: item.delete_item() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditableItem): item.setEnabled(value) def clear_extreme_points(self): if self._extreme_points.qsize() > 0: for pt in self._extreme_points.queue: self._scene.removeItem(pt) self._extreme_points.queue.clear()
class GridItem(QGraphicsRectItem): def __init__(self, part_item, grid_type): """Summary previous_grid_bounds (tuple): a tuple corresponding to the bounds of the grid. Args: part_item (TYPE): Description grid_type (TYPE): Description """ super(GridItem, self).__init__(parent=part_item) self.setFlag(QGraphicsItem.ItemClipsChildrenToShape) self._path = None self.part_item = part_item self._path = QGraphicsPathItem(self) self.dots = (styles.DOT_SIZE, styles.DOT_SIZE / 2) # self.allow_snap = part_item.window().action_vhelix_snap.isChecked() self._draw_gridpoint_coordinates = False self.draw_lines = False self.points = [] self.points_dict = dict() self.previous_grid_bounds = None self.bounds = None self.grid_type = None self.setPen( getPenObj(styles.GRAY_STROKE, styles.EMPTY_HELIX_STROKE_WIDTH)) self.setGridType(grid_type) self.previous_grid_type = grid_type # end def def updateGrid(self): """Recreates the grid according to the latest part_item outline rect. """ part_item = self.part_item part = part_item.part() radius = part.radius() self.bounds = part_item.bounds() self.removePoints() self.setRect(self.part_item.outline.rect()) if self.grid_type == GridType.HONEYCOMB: self.createHoneycombGrid(part_item, radius, self.bounds) elif self.grid_type == GridType.SQUARE: self.createSquareGrid(part_item, radius, self.bounds) else: self._path.setPath(QPainterPath()) # end def def setGridType(self, grid_type): """Sets the grid type. See cadnano.cnenum.GridType. Args: grid_type (GridType): NONE, HONEYCOMB, or SQUARE """ self.grid_type = grid_type self.updateGrid() # end def def createHoneycombGrid(self, part_item, radius, bounds): """Instantiate an area of griditems arranged on a honeycomb lattice. Args: part_item (TYPE): Description radius (TYPE): Description bounds (TYPE): Description Returns: TYPE: Description """ doLattice = HoneycombDnaPart.latticeCoordToPositionXY doPosition = HoneycombDnaPart.positionToLatticeCoordRound isEven = HoneycombDnaPart.isEvenParity x_l, x_h, y_l, y_h = bounds x_l = x_l + HoneycombDnaPart.PAD_GRID_XL x_h = x_h + HoneycombDnaPart.PAD_GRID_XH y_h = y_h + HoneycombDnaPart.PAD_GRID_YL y_l = y_l + HoneycombDnaPart.PAD_GRID_YH dot_size, half_dot_size = self.dots sf = part_item.scale_factor points = self.points row_l, col_l = doPosition(radius, x_l, -y_l, False, False, scale_factor=sf) row_h, col_h = doPosition(radius, x_h, -y_h, True, True, scale_factor=sf) redo_neighbors = (row_l, col_l, row_h, col_h) != self.previous_grid_bounds or\ self.previous_grid_type != self.grid_type self.previous_grid_type = self.grid_type path = QPainterPath() is_pen_down = False draw_lines = self.draw_lines if redo_neighbors: point_coordinates = dict() neighbor_map = dict() self.points_dict = dict() for row in range(row_l, row_h): for column in range(col_l, col_h + 1): x, y = doLattice(radius, row, column, scale_factor=sf) if draw_lines: if is_pen_down: path.lineTo(x, -y) else: is_pen_down = True path.moveTo(x, -y) """ +x is Left and +y is down origin of ellipse is Top Left corner so we subtract half in X and subtract in y """ pt = GridPoint(x - half_dot_size, -y - half_dot_size, dot_size, self, coord=(row, column)) if self._draw_gridpoint_coordinates: font = QFont(styles.THE_FONT) path.addText(x - 10, -y + 5, font, "%s,%s" % (-row, column)) pt.setPen( getPenObj(styles.GRAY_STROKE, styles.EMPTY_HELIX_STROKE_WIDTH)) # if x == 0 and y == 0: # pt.setBrush(getBrushObj(Qt.gray)) points.append(pt) self.points_dict[(-row, column)] = pt if redo_neighbors: point_coordinates[(-row, column)] = (x, -y) # This is reversed since the Y is mirrored if not HoneycombDnaPart.isEvenParity(row, column): neighbor_map[(-row, column)] = [(-row - 1, column), (-row, column + 1), (-row, column - 1)] else: neighbor_map[(-row, column)] = [(-row + 1, column), (-row, column - 1), (-row, column + 1)] self.previous_grid_bounds = (row_l, col_l, row_h, col_h) is_pen_down = False if draw_lines: for column in range(col_l, col_h + 1): for row in range(row_l, row_h): x, y = doLattice(radius, row, column, scale_factor=sf) if is_pen_down and isEven(row, column): path.lineTo(x, -y) is_pen_down = False else: is_pen_down = True path.moveTo(x, -y) is_pen_down = False # end for j self._path.setPath(path) if redo_neighbors: self.part_item.setNeighborMap(neighbor_map=neighbor_map) self.part_item.setPointMap(point_map=point_coordinates) # end def def createSquareGrid(self, part_item, radius, bounds): """Instantiate an area of griditems arranged on a square lattice. Args: part_item (TYPE): Description radius (TYPE): Description bounds (TYPE): Description Returns: TYPE: Description """ doLattice = SquareDnaPart.latticeCoordToPositionXY doPosition = SquareDnaPart.positionToLatticeCoordRound x_l, x_h, y_l, y_h = bounds x_l = x_l + SquareDnaPart.PAD_GRID_XL x_h = x_h + SquareDnaPart.PAD_GRID_XH y_h = y_h + SquareDnaPart.PAD_GRID_YL y_l = y_l + SquareDnaPart.PAD_GRID_YH dot_size, half_dot_size = self.dots sf = part_item.scale_factor points = self.points row_l, col_l = doPosition(radius, x_l, -y_l, scale_factor=sf) row_h, col_h = doPosition(radius, x_h, -y_h, scale_factor=sf) redo_neighbors = (row_l, col_l, row_h, col_h) != \ self.previous_grid_bounds or self.previous_grid_type != self.grid_type self.previous_grid_type = self.grid_type if redo_neighbors: point_map = dict() neighbor_map = dict() path = QPainterPath() is_pen_down = False draw_lines = self.draw_lines for row in range(row_l, row_h + 1): for column in range(col_l, col_h + 1): x, y = doLattice(radius, row, column, scale_factor=sf) if draw_lines: if is_pen_down: path.lineTo(x, -y) else: is_pen_down = True path.moveTo(x, -y) """ +x is Left and +y is down origin of ellipse is Top Left corner so we subtract half in X and subtract in y """ pt = GridPoint(x - half_dot_size, -y - half_dot_size, dot_size, self, coord=(row, column)) if self._draw_gridpoint_coordinates: font = QFont(styles.THE_FONT) path.addText(x - 10, -y + 5, font, "%s,%s" % (-row, column)) pt.setPen( getPenObj(styles.GRAY_STROKE, styles.EMPTY_HELIX_STROKE_WIDTH)) # if x == 0 and y == 0: # pt.setBrush(getBrushObj(Qt.gray)) points.append(pt) self.points_dict[(-row, column)] = pt if redo_neighbors: point_map[(-row, column)] = (x, -y) neighbor_map[(-row, column)] = [(-row, column + 1), (-row, column - 1), (-row - 1, column), (-row + 1, column)] self.previous_grid_bounds = (row_l, col_l, row_h, col_h) is_pen_down = False # pen up # DO VERTICAL LINES if draw_lines: for column in range(col_l, col_h + 1): for row in range(row_l, row_h + 1): x, y = doLattice(radius, row, column, scale_factor=sf) if is_pen_down: path.lineTo(x, -y) else: is_pen_down = True path.moveTo(x, -y) is_pen_down = False # pen up self._path.setPath(path) if redo_neighbors: self.part_item.setNeighborMap(neighbor_map=neighbor_map) self.part_item.setPointMap(point_map=point_map) # end def def removePoints(self): """Remove all points from the grid. """ points = self.points scene = self.scene() while points: scene.removeItem(points.pop()) self.points_dict = dict() # end def def showCreateHint(self, coord, next_idnums=(0, 1), show_hint=True, color=None): point_item = self.points_dict.get(coord, None) if point_item is None: return if show_hint is False: point_item.showCreateHint(show_hint=False) if point_item: row, column = coord if self.grid_type is GridType.HONEYCOMB: parity = 0 if HoneycombDnaPart.isOddParity( row=row, column=column) else 1 elif self.grid_type is GridType.SQUARE: parity = 0 if SquareDnaPart.isEvenParity(row=row, column=column) else 1 else: return id_num = next_idnums[1] if parity else next_idnums[0] point_item.showCreateHint(id_num=id_num, show_hint=show_hint, color=color) return parity == 1 # end def def setPath(self, path): assert isinstance(path, QPainterPath) self._path = path # end def def path(self): return self._path
class TableItem(KineticsDisplayItem): defaultWidth = 30 defaultHeight = 30 defaultPenWidth = 2 name = constants.ITEM def __init__(self, *args, **kwargs): KineticsDisplayItem.__init__(self, *args, **kwargs) points = [ QtCore.QPointF(0, TableItem.defaultWidth / 2), QtCore.QPointF(TableItem.defaultHeight / 2 - 2, 0), QtCore.QPointF(TableItem.defaultWidth / 2 + 2, 0), QtCore.QPointF(TableItem.defaultWidth, TableItem.defaultHeight / 2), ] path = QtGui.QPainterPath() path.moveTo(points[0]) for p in points[1:]: path.lineTo(p) path.moveTo(p) path.moveTo(0, 0) path.lineTo(TableItem.defaultWidth, 0) path.moveTo(-(TableItem.defaultWidth / 3), TableItem.defaultHeight / 4) path.lineTo((TableItem.defaultWidth + 10), TableItem.defaultHeight / 4) self.gobj = QGraphicsPathItem(path, self) #self.gobj.setToolTip("Need to see what to show unlike conc/nint for pool") tabledoc = (moose.element(self.mobj.path)).outputValue self.gobj.setToolTip(str(tabledoc)) self.gobj.setPen( QtGui.QPen(QtCore.Qt.black, 2, Qt.Qt.SolidLine, Qt.Qt.RoundCap, Qt.Qt.RoundJoin)) self.gobj.mobj = self.mobj def refresh(self, scale): defaultWidth = TableItem.defaultWidth * scale defaultHeight = TableItem.defaultHeight * scale points = [ QtCore.QPointF(0, defaultWidth / 2), QtCore.QPointF(defaultHeight / 2 - 2, 0), QtCore.QPointF(defaultWidth / 2 + 2, 0), QtCore.QPointF(defaultWidth, defaultHeight / 2) ] path = QtGui.QPainterPath() path.moveTo(points[0]) for p in points[1:]: path.lineTo(p) path.moveTo(p) path.moveTo(0, 0) path.lineTo(defaultWidth, 0) path.moveTo(-(defaultWidth / 3), defaultHeight / 4) path.lineTo((defaultWidth + 10), defaultHeight / 4) self.gobj.setPath(path) TablePen = self.gobj.pen() tableWidth = TableItem.defaultPenWidth * scale TablePen.setWidth(tableWidth) self.gobj.setPen(TablePen) def setDisplayProperties(self, x, y, textcolor, bgcolor): """Set the display properties of this item.""" # TODO: check the table bounding reactangle b'cos selection looks ugly self.setGeometry(x, y, self.gobj.boundingRect().width(), self.gobj.boundingRect().height())
class TextAnnotation(Annotation): """Text annotation item for the canvas scheme. """ editingFinished = Signal() """Emitted when the editing is finished (i.e. the item loses focus).""" textEdited = Signal() """Emitted when the edited text changes.""" def __init__(self, parent=None, **kwargs): Annotation.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFocusPolicy(Qt.ClickFocus) self.__textMargins = (2, 2, 2, 2) rect = self.geometry().translated(-self.pos()) ################################ # PyQt 5.10 crashes because of this call (3) #self.__framePathItem = QGraphicsPathItem(self) self.__framePathItem = QGraphicsPathItem(None) self.__framePathItem.setPen(QPen(Qt.NoPen)) self.__textItem = GraphicsTextEdit(self) self.__textItem.setPlaceholderText(self.tr("Enter text here")) self.__textItem.setPos(2, 2) self.__textItem.setTextWidth(rect.width() - 4) self.__textItem.setTabChangesFocus(True) self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.setFont(self.font()) self.__textInteractionFlags = Qt.NoTextInteraction layout = self.__textItem.document().documentLayout() layout.documentSizeChanged.connect(self.__onDocumentSizeChanged) self.__updateFrame() def adjustSize(self): """Resize to a reasonable size. """ self.__textItem.setTextWidth(-1) self.__textItem.adjustSize() size = self.__textItem.boundingRect().size() left, top, right, bottom = self.textMargins() geom = QRectF(self.pos(), size + QSizeF(left + right, top + bottom)) self.setGeometry(geom) def setFramePen(self, pen): """Set the frame pen. By default Qt.NoPen is used (i.e. the frame is not shown). """ self.__framePathItem.setPen(pen) def framePen(self): """Return the frame pen. """ return self.__framePathItem.pen() def setFrameBrush(self, brush): """Set the frame brush. """ self.__framePathItem.setBrush(brush) def frameBrush(self): """Return the frame brush. """ return self.__framePathItem.brush() def setPlainText(self, text): """Set the annotation plain text. """ self.__textItem.setPlainText(text) def toPlainText(self): return self.__textItem.toPlainText() def setHtml(self, text): """Set the annotation rich text. """ self.__textItem.setHtml(text) def toHtml(self): return self.__textItem.toHtml() def setDefaultTextColor(self, color): """Set the default text color. """ self.__textItem.setDefaultTextColor(color) def defaultTextColor(self): return self.__textItem.defaultTextColor() def setTextMargins(self, left, top, right, bottom): """Set the text margins. """ margins = (left, top, right, bottom) if self.__textMargins != margins: self.__textMargins = margins self.__textItem.setPos(left, top) self.__textItem.setTextWidth( max(self.geometry().width() - left - right, 0)) def textMargins(self): """Return the text margins. """ return self.__textMargins def document(self): """Return the QTextDocument instance used internally. """ return self.__textItem.document() def setTextCursor(self, cursor): self.__textItem.setTextCursor(cursor) def textCursor(self): return self.__textItem.textCursor() def setTextInteractionFlags(self, flags): self.__textInteractionFlags = flags def textInteractionFlags(self): return self.__textInteractionFlags def setDefaultStyleSheet(self, stylesheet): self.document().setDefaultStyleSheet(stylesheet) def mouseDoubleClickEvent(self, event): Annotation.mouseDoubleClickEvent(self, event) if event.buttons() == Qt.LeftButton and \ self.__textInteractionFlags & Qt.TextEditable: self.startEdit() def startEdit(self): """Start the annotation text edit process. """ self.__textItem.setTextInteractionFlags(self.__textInteractionFlags) self.__textItem.setFocus(Qt.MouseFocusReason) # Install event filter to find out when the text item loses focus. self.__textItem.installSceneEventFilter(self) self.__textItem.document().contentsChanged.connect(self.textEdited) def endEdit(self): """End the annotation edit. """ self.__textItem.setTextInteractionFlags(Qt.NoTextInteraction) self.__textItem.removeSceneEventFilter(self) self.__textItem.document().contentsChanged.disconnect(self.textEdited) self.editingFinished.emit() def __onDocumentSizeChanged(self, size): # The size of the text document has changed. Expand the text # control rect's height if the text no longer fits inside. try: rect = self.geometry() _, top, _, bottom = self.textMargins() if rect.height() < (size.height() + bottom + top): rect.setHeight(size.height() + bottom + top) self.setGeometry(rect) except Exception: log.error("error in __onDocumentSizeChanged", exc_info=True) def __updateFrame(self): rect = self.geometry() rect.moveTo(0, 0) path = QPainterPath() path.addRect(rect) self.__framePathItem.setPath(path) def resizeEvent(self, event): width = event.newSize().width() left, _, right, _ = self.textMargins() self.__textItem.setTextWidth(max(width - left - right, 0)) self.__updateFrame() QGraphicsWidget.resizeEvent(self, event) def sceneEventFilter(self, obj, event): if obj is self.__textItem and event.type() == QEvent.FocusOut: self.__textItem.focusOutEvent(event) self.endEdit() return True return Annotation.sceneEventFilter(self, obj, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: if self.isSelected(): self.setFramePen(QPen(Qt.DashDotLine)) else: self.setFramePen(QPen(Qt.NoPen)) return Annotation.itemChange(self, change, value) def changeEvent(self, event): if event.type() == QEvent.FontChange: self.__textItem.setFont(self.font()) Annotation.changeEvent(self, event)
class PreXoverItem(QGraphicsRectItem): """A PreXoverItem exists between a single 'from' VirtualHelixItem index and zero or more 'to' VirtualHelixItem Indices Attributes: adapter (:obj:`PropertyWrapperObject`): Description idx (int): the base index within the virtual helix is_fwd (bool): is this a forward strand? prexoveritem_manager (:obj:`PreXoverManager`): Manager of the PreXoverItems to_vh_id_num (int): Virtual Helix number this Xover point might connect to """ FILTER_NAME = "xover" def __init__(self, from_virtual_helix_item: PathVirtualHelixItemT, is_fwd: bool, from_index: int, nearby_idxs: List[int], to_vh_id_num: int, prexoveritem_manager: PreXoverManagerT): """Summary Args: from_virtual_helix_item: Description is_fwd: is this a forward strand? from_index: index of the Virtual Helix this xover is coming from nearby_idxs: to_vh_id_num: Virtual Helix number this Xover point might connect to prexoveritem_manager: Manager of the PreXoverItems """ super(QGraphicsRectItem, self).__init__(BASE_RECT, from_virtual_helix_item) self.adapter = PropertyWrapperObject(self) self._tick_marks = QGraphicsPathItem(self) self._tick_marks.setAcceptHoverEvents(True) self._bond_item = QGraphicsPathItem(self) self._bond_item.hide() self._label = PreXoverLabel(is_fwd, self) self._path = QGraphicsPathItem() self.setZValue(styles.ZPREXOVERITEM) self.setPen(getNoPen()) self.resetItem(from_virtual_helix_item, is_fwd, from_index, nearby_idxs, to_vh_id_num, prexoveritem_manager) self._getActiveTool = from_virtual_helix_item.viewroot().manager.activeToolGetter # end def def shutdown(self): """Summary """ self.setBrush(getNoBrush()) self.to_vh_id_num = None self.adapter.resetAnimations() self.setAcceptHoverEvents(False) self.hide() # end def def resetItem(self, from_virtual_helix_item: PathVirtualHelixItemT, is_fwd: bool, from_index: int, nearby_idxs: List[int], to_vh_id_num: int, prexoveritem_manager: PreXoverManagerT): """Update this pooled PreXoverItem with current info. Called by PreXoverManager. Args: from_virtual_helix_item: the associated vh_item is_fwd: True if associated with fwd strand, False if rev strand from_index: idx of associated vh nearby_idxs: to_vh_id_num: id_num of the other vh prexoveritem_manager: the manager """ # to_vh_item = from_virtual_helix_item.partItem().idToVirtualHelixItem(to_vh_id_num) self.setParentItem(from_virtual_helix_item) # self.setParentItem(to_vh_item) self.resetTransform() self._id_num = from_virtual_helix_item.idNum() self._model_part = from_virtual_helix_item.part() self.idx = from_index self.is_low = False self.is_high = False self.nearby_idxs = nearby_idxs self.is_fwd = is_fwd self.color = None self.is3p = None self.enter_pos = None self.exit_pos = None self.to_vh_id_num = to_vh_id_num self._label_txt = None self.prexoveritem_manager = prexoveritem_manager # todo: check here if xover present and disable result = self.setPathAppearance(from_virtual_helix_item) if result: self.setBrush(getNoBrush()) if is_fwd: self.setPos(from_index*BASE_WIDTH, -BASE_WIDTH - 0.1*BASE_WIDTH) else: self.setPos(from_index*BASE_WIDTH, 2*BASE_WIDTH) self.show() # label self._label_txt = lbt = None if to_vh_id_num is None else str(to_vh_id_num) self._label.resetItem(is_fwd, self.color) self.setLabel(text=lbt) # bond line bonditem = self._bond_item bonditem.setPen(getPenObj( self.color, styles.PREXOVER_STROKE_WIDTH, penstyle=Qt.DotLine)) bonditem.hide() # end def def setPathAppearance(self, from_virtual_helix_item: PathVirtualHelixItemT) -> bool: """Sets the PainterPath according to the index (low = Left, high = Right) and strand position (top = Up, bottom = Down). Args: from_virtual_helix_item: """ part = self._model_part idx = self.idx is_fwd = self.is_fwd id_num = self._id_num strand_type = StrandEnum.FWD if is_fwd else StrandEnum.REV # relative position info bpr = from_virtual_helix_item.getProperty('bases_per_repeat') self.is_low = is_low = idx+1 in self.nearby_idxs or (idx+1) % bpr in self.nearby_idxs self.is_high = is_high = idx-1 in self.nearby_idxs or (idx-1) % bpr in self.nearby_idxs # check strand for xover and color if part.hasStrandAtIdx(id_num, idx)[strand_type]: strand = part.getStrand(self.is_fwd, id_num, idx) if strand.hasXoverAt(idx): return False self.color = strand.getColor() if strand is not None else EMPTY_COL else: self.color = EMPTY_COL if is_low and is_high: path = (_FWD_DUAL_PATH, _REV_DUAL_PATH)[strand_type] raise NotImplementedError("Dual xovers not yet supported") elif is_low: path = (_FWD_LO_PATH, _REV_LO_PATH)[strand_type] self.is3p = True if is_fwd else False self.enter_pos = _FWD_LO_PTS[0][-1] if is_fwd else _REV_LO_PTS[0][-1] self.exit_pos = _FWD_LO_PTS[0][-1] if is_fwd else _REV_LO_PTS[0][-1] elif is_high: path = (_FWD_HI_PATH, _REV_HI_PATH)[strand_type] self.is3p = False if is_fwd else True self.enter_pos = _FWD_HI_PTS[0][-1] if is_fwd else _REV_HI_PTS[0][-1] self.exit_pos = _FWD_HI_PTS[0][-1] if is_fwd else _REV_HI_PTS[0][-1] else: # print("unpaired PreXoverItem at {}[{}]".format(self._id_num, self.idx), self.nearby_idxs) return False self._tick_marks.setPen(getPenObj( self.color, styles.PREXOVER_STROKE_WIDTH, capstyle=Qt.FlatCap, joinstyle=Qt.RoundJoin)) self._tick_marks.setPath(path) self._tick_marks.show() return True # end def ### ACCESSORS ### def color(self) -> str: """The PreXoverItem's color, derived from the associated strand's oligo. Returns: str: color in hex code """ return self.color def getInfo(self) -> ABInfoT: """ Returns: Tuple: (from_id_num, is_fwd, from_index, to_vh_id_num) """ return (self._id_num, self.is_fwd, self.idx, self.to_vh_id_num) def destroyItem(self): """Removes animation adapter, label, bond_item, and this item from scene. """ scene = self.scene() self.adapter.destroyItem() if scene: scene.removeItem(self._label) self._label = None scene.removeItem(self._bond_item) self._bond_item = None self.adapter.resetAnimations() self.adapter = None scene.removeItem(self) # end defS ### EVENT HANDLERS ### def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): """Only ``if enableActive(True)`` is called hover and key events disabled by default Args: event: the hover event """ if self._getActiveTool().methodPrefix() != "selectTool": return self.setFocus(Qt.MouseFocusReason) self.prexoveritem_manager.updateModelActiveBaseInfo(self.getInfo()) self.setActiveHovered(True) status_string = "%d[%d]" % (self._id_num, self.idx) self.parentItem().window().statusBar().showMessage(status_string) return QGraphicsItem.hoverEnterEvent(self, event) # end def def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): """Summary Args: event (QGraphicsSceneHoverEvent): the hover event """ self.prexoveritem_manager.updateModelActiveBaseInfo(None) self.setActiveHovered(False) self.clearFocus() self.parentItem().window().statusBar().showMessage("") return QGraphicsItem.hoverLeaveEvent(self, event) # end def def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """TODO: NEED TO ADD FILTER FOR A CLICK ON THE 3' MOST END OF THE XOVER TO DISALLOW OR HANDLE DIFFERENTLY """ viewroot = self.parentItem().viewroot() current_filter_set = viewroot.selectionFilterSet() if (self._getActiveTool().methodPrefix() != "selectTool" or (self.FILTER_NAME not in current_filter_set) ): return part = self._model_part is_fwd = self.is_fwd if self.is3p: strand5p = part.getStrand(is_fwd, self._id_num, self.idx) strand3p = part.getStrand(not is_fwd, self.to_vh_id_num, self.idx) else: strand5p = part.getStrand(not is_fwd, self.to_vh_id_num, self.idx) strand3p = part.getStrand(is_fwd, self._id_num, self.idx) if strand5p is None or strand3p is None: return # print(strand3p, strand5p) part.createXover(strand5p, self.idx, strand3p, self.idx) nkey = (self.to_vh_id_num, not is_fwd, self.idx) npxi = self.prexoveritem_manager.neighbor_prexover_items.get(nkey, None) if npxi: npxi.shutdown() self.shutdown() part.setActiveVirtualHelix(self._id_num, is_fwd, self.idx) # self.prexoveritem_manager.handlePreXoverClick(self) def keyPressEvent(self, event: QKeyEvent): """ Args: event: Description """ self.prexoveritem_manager.handlePreXoverKeyPress(event.key()) # end def ### PUBLIC SUPPORT METHODS ### def setLabel(self, text: str = None, outline: bool = False): """Summary Args: text: Default is ``None`` outline: Default is ``False`` """ if text: self._label.setTextAndStyle(text=text, outline=outline) self._label.show() else: self._label.hide() # end def def animate(self, item: QGraphicsItem, property_name: str, duration: int, start_value, end_value): """ Args: item: Description property_name: Description duration: Description start_value (QVariant): Description end_value (QVariant): Description """ b_name = property_name.encode('ascii') anim = item.adapter.getRef(property_name) if anim is None: anim = QPropertyAnimation(item.adapter, b_name) item.adapter.saveRef(property_name, anim) anim.setDuration(duration) anim.setStartValue(start_value) anim.setEndValue(end_value) anim.start() # end def def setActiveHovered(self, is_active: bool): """Rotate phosphate Triangle if `self.to_vh_id_num` is not `None` Args: is_active: whether or not the PreXoverItem is parented to the active VirtualHelixItem """ pass # end def def enableActive(self, is_active: bool, to_vh_id_num: int = None): """Call on PreXoverItems created on the active VirtualHelixItem Args: is_active: Description to_vh_id_num: Default is ``None`` """ if is_active: self.to_vh_id_num = to_vh_id_num self.setAcceptHoverEvents(True) if to_vh_id_num is None: self.setLabel(text=None) else: self.setLabel(text=str(to_vh_id_num)) else: self.setBrush(getNoBrush()) self.setAcceptHoverEvents(False) def activateNeighbor(self, active_prexoveritem: 'PreXoverItem', shortcut: str = None): """Draws a quad line starting from the item5p to the item3p. To be called with whatever the active_prexoveritem is for the parts `active_base`. Args: active_prexoveritem: Description shortcut: Default is None """ if self._getActiveTool().methodPrefix() != "selectTool": return if self.is3p and not active_prexoveritem.is3p: item5p = active_prexoveritem item3p = self elif not self.is3p and active_prexoveritem.is3p: item5p = self item3p = active_prexoveritem else: return same_parity = self.is_fwd == active_prexoveritem.is_fwd p1 = item5p._tick_marks.scenePos() + item5p.exit_pos p2 = item3p._tick_marks.scenePos() + item3p.exit_pos c1 = QPointF() # case 1: same parity if same_parity: dy = abs(p2.y() - p1.y()) c1.setX(p1.x() + _X_SCALE * dy) c1.setY(0.5 * (p1.y() + p2.y())) # case 2: different parity else: if item3p.is_fwd: c1.setX(p1.x() - _X_SCALE * abs(p2.y() - p1.y())) else: c1.setX(p1.x() + _X_SCALE * abs(p2.y() - p1.y())) c1.setY(0.5 * (p1.y() + p2.y())) pp = QPainterPath() pp.moveTo(self._tick_marks.mapFromScene(p1)) pp.quadTo(self._tick_marks.mapFromScene(c1), self._tick_marks.mapFromScene(p2)) # pp.cubicTo(c1, c2, self._tick_marks.mapFromScene(p2)) self._bond_item.setPath(pp) self._bond_item.show() # end def def deactivateNeighbor(self): """Summary """ if self.isVisible(): self._bond_item.hide() self.setLabel(text=self._label_txt)
class MapData(dict): def __init__(self, zone=None): super().__init__() self.zone = zone self.raw = {'lines': [], 'poi': [], 'grid': []} self.geometry = None # MapGeometry self.players = {} self.spawns = [] self.way_point = None self.grid = None if self.zone is not None: self._load() def _load(self): # Get list of all map files for current zone map_file_name = MapData.get_zone_dict()[self.zone.strip().lower()] extensions = ['.txt', '_1.txt', '_2.txt', '_3.txt', '_4.txt', '_5.txt'] maps = [ os.path.join(MAP_FILES_LOCATION, m) for m in [(map_file_name + e) for e in extensions] if os.path.exists(os.path.join(MAP_FILES_LOCATION, m)) ] all_x, all_y, all_z = [], [], [] # TODO: Remove the references to raw # Create Lines and Points for map_file in maps: with open(map_file, 'r') as f: for line in f.readlines(): line_type = line.lower()[0:1] data = [value.strip() for value in line[1:].split(',')] if line_type == 'l': # line x1, y1, z1, x2, y2, z2 = list(map(float, data[0:6])) self.raw['lines'].append( MapLine(x1=x1, y1=y1, z1=z1, x2=x2, y2=y2, z2=z2, color=self.color_transform( QColor(int(data[6]), int(data[7]), int(data[8]))))) all_x.extend((x1, x2)) all_y.extend((y1, y2)) all_z.append(min(z1, z2)) # if abs(z1 - z2) < 2: # if z1 == z2: # all_z.extend((z1, z2)) elif line_type == 'p': # point x, y, z = map(float, data[0:3]) self.raw['poi'].append( MapPoint(x=x, y=y, z=z, size=int(data[6]), text=str(data[7]), color=self.color_transform( QColor(int(data[3]), int(data[4]), int(data[5]))))) # Create Grid Lines lowest_x, highest_x, lowest_y, highest_y, lowest_z, highest_z = min( all_x), max(all_x), min(all_y), max(all_y), min(all_z), max(all_z) left, right = int(math.floor(lowest_x / 1000) * 1000), int( math.ceil(highest_x / 1000) * 1000) top, bottom = int(math.floor(lowest_y / 1000) * 1000), int( math.ceil(highest_y / 1000) * 1000) for number in range(left, right + 1000, 1000): self.raw['grid'].append( MapLine(x1=number, x2=number, y1=top, y2=bottom, z1=0, z2=0, color=QColor(255, 255, 255, 25))) for number in range(top, bottom + 1000, 1000): self.raw['grid'].append( MapLine(y1=number, y2=number, x1=left, x2=right, z1=0, z2=0, color=QColor(255, 255, 255, 25))) self.grid = QGraphicsPathItem() line_path = QPainterPath() for line in self.raw['grid']: line_path.moveTo(line.x1, line.y1) line_path.lineTo(line.x2, line.y2) self.grid.setPath(line_path) self.grid.setPen( QPen(line.color, config.data['maps']['grid_line_width'])) self.grid.setZValue(0) # Get z levels counter = Counter(all_z) # bunch together zgroups based on peaks with floor being low point before rise z_groups = [] last_value = None first_run = True for z in sorted(counter.items(), key=lambda x: x[0]): if last_value is None: last_value = z continue if (abs(last_value[0] - z[0]) < 20) or z[1] < 8: last_value = (last_value[0], last_value[1] + z[1]) else: if first_run: first_run = False if last_value[1] < 40 or abs(last_value[0] - z[0]) < 18: last_value = z continue z_groups.append(last_value[0]) last_value = z # get last iteration if last_value[1] > 50: z_groups.append(last_value[0]) self._z_groups = z_groups # Create QGraphicsPathItem for lines seperately to retain colors temp_dict = {} for l in self.raw['lines']: lz = min(l.z1, l.z2) lz = self.get_closest_z_group(lz) if not temp_dict.get(lz, None): temp_dict[lz] = {'paths': {}} lc = l.color.getRgb() if not temp_dict[lz]['paths'].get(lc, None): path_item = QGraphicsPathItem() path_item.setPen( QPen(l.color, config.data['maps']['line_width'])) temp_dict[lz]['paths'][lc] = path_item path = temp_dict[lz]['paths'][lc].path() path.moveTo(l.x1, l.y1) path.lineTo(l.x2, l.y2) temp_dict[lz]['paths'][lc].setPath(path) # Group QGraphicsPathItems into QGraphicsItemGroups and update self for z in temp_dict.keys(): item_group = QGraphicsItemGroup() for (_, path) in temp_dict[z]['paths'].items(): item_group.addToGroup(path) self[z] = {'paths': None, 'poi': []} self[z]['paths'] = item_group # Create Points of Interest for p in self.raw['poi']: z = self.get_closest_z_group(p.z) self[z]['poi'].append(PointOfInterest(location=p)) self.geometry = MapGeometry( lowest_x=lowest_x, highest_x=highest_x, lowest_y=lowest_y, highest_y=highest_y, lowest_z=lowest_z, highest_z=highest_z, center_x=int(highest_x - (highest_x - lowest_x) / 2), center_y=int(highest_y - (highest_y - lowest_y) / 2), width=int(highest_x - lowest_x), height=int(highest_y - lowest_y), z_groups=z_groups) def get_closest_z_group(self, z): closest = min(self._z_groups, key=lambda x: abs(x - z)) if z < closest: lower_index = self._z_groups.index(closest) - 1 if lower_index > -1: closest = self._z_groups[lower_index] return closest @staticmethod def get_zone_dict(): # Load Map Pairs from map_keys.ini zone_dict = {} with open(MAP_KEY_FILE, 'r') as file: for line in file.readlines(): values = line.split('=') zone_dict[values[0].strip()] = values[1].strip() return zone_dict @staticmethod def color_transform(color): lightness = color.lightness() if lightness == 0: return QColor(255, 255, 255) elif (color.red == color.green == color.blue): return QColor(255, 255, 255) elif lightness < 150: return color.lighter(150) return color
class PreXoverItem(QGraphicsRectItem): """A PreXoverItem exists between a single 'from' VirtualHelixItem index and zero or more 'to' VirtualHelixItem Indices Attributes: adapter (:obj:`PropertyWrapperObject`): Description idx (int): the base index within the virtual helix is_fwd (bool): is this a forward strand? prexoveritem_manager (:obj:`PreXoverManager`): Manager of the PreXoverItems to_vh_id_num (int): Virtual Helix number this Xover point might connect to """ FILTER_NAME = "xover" def __init__(self, from_virtual_helix_item: PathVirtualHelixItemT, is_fwd: bool, from_index: int, nearby_idxs: List[int], to_vh_id_num: int, prexoveritem_manager: PreXoverManagerT): """Summary Args: from_virtual_helix_item: Description is_fwd: is this a forward strand? from_index: index of the Virtual Helix this xover is coming from nearby_idxs: to_vh_id_num: Virtual Helix number this Xover point might connect to prexoveritem_manager: Manager of the PreXoverItems """ super(QGraphicsRectItem, self).__init__(BASE_RECT, from_virtual_helix_item) self.adapter = PropertyWrapperObject(self) self._tick_marks = QGraphicsPathItem(self) self._tick_marks.setAcceptHoverEvents(True) self._bond_item = QGraphicsPathItem(self) self._bond_item.hide() self._label = PreXoverLabel(is_fwd, self) self._path = QGraphicsPathItem() self.setZValue(styles.ZPREXOVERITEM) self.setPen(getNoPen()) self.resetItem(from_virtual_helix_item, is_fwd, from_index, nearby_idxs, to_vh_id_num, prexoveritem_manager) self._getActiveTool = from_virtual_helix_item.viewroot( ).manager.activeToolGetter # end def def shutdown(self): """Summary """ self.setBrush(getNoBrush()) self.to_vh_id_num = None self.adapter.resetAnimations() self.setAcceptHoverEvents(False) self.hide() # end def def resetItem(self, from_virtual_helix_item: PathVirtualHelixItemT, is_fwd: bool, from_index: int, nearby_idxs: List[int], to_vh_id_num: int, prexoveritem_manager: PreXoverManagerT): """Update this pooled PreXoverItem with current info. Called by PreXoverManager. Args: from_virtual_helix_item: the associated vh_item is_fwd: True if associated with fwd strand, False if rev strand from_index: idx of associated vh nearby_idxs: to_vh_id_num: id_num of the other vh prexoveritem_manager: the manager """ # to_vh_item = from_virtual_helix_item.partItem().idToVirtualHelixItem(to_vh_id_num) self.setParentItem(from_virtual_helix_item) # self.setParentItem(to_vh_item) self.resetTransform() self._id_num = from_virtual_helix_item.idNum() self._model_part = from_virtual_helix_item.part() self.idx = from_index self.is_low = False self.is_high = False self.nearby_idxs = nearby_idxs self.is_fwd = is_fwd self.color = None self.is3p = None self.enter_pos = None self.exit_pos = None self.to_vh_id_num = to_vh_id_num self._label_txt = None self.prexoveritem_manager = prexoveritem_manager # todo: check here if xover present and disable result = self.setPathAppearance(from_virtual_helix_item) if result: self.setBrush(getNoBrush()) if is_fwd: self.setPos(from_index * BASE_WIDTH, -BASE_WIDTH - 0.1 * BASE_WIDTH) else: self.setPos(from_index * BASE_WIDTH, 2 * BASE_WIDTH) self.show() # label self._label_txt = lbt = None if to_vh_id_num is None else str( to_vh_id_num) self._label.resetItem(is_fwd, self.color) self.setLabel(text=lbt) # bond line bonditem = self._bond_item bonditem.setPen( getPenObj(self.color, styles.PREXOVER_STROKE_WIDTH, penstyle=Qt.DotLine)) bonditem.hide() # end def def setPathAppearance( self, from_virtual_helix_item: PathVirtualHelixItemT) -> bool: """Sets the PainterPath according to the index (low = Left, high = Right) and strand position (top = Up, bottom = Down). Args: from_virtual_helix_item: """ part = self._model_part idx = self.idx is_fwd = self.is_fwd id_num = self._id_num strand_type = StrandEnum.FWD if is_fwd else StrandEnum.REV # relative position info bpr = from_virtual_helix_item.getProperty('bases_per_repeat') self.is_low = is_low = idx + 1 in self.nearby_idxs or ( idx + 1) % bpr in self.nearby_idxs self.is_high = is_high = idx - 1 in self.nearby_idxs or ( idx - 1) % bpr in self.nearby_idxs # check strand for xover and color if part.hasStrandAtIdx(id_num, idx)[strand_type]: strand = part.getStrand(self.is_fwd, id_num, idx) if strand.hasXoverAt(idx): return False self.color = strand.getColor() if strand is not None else EMPTY_COL else: self.color = EMPTY_COL if is_low and is_high: path = (_FWD_DUAL_PATH, _REV_DUAL_PATH)[strand_type] raise NotImplementedError("Dual xovers not yet supported") elif is_low: path = (_FWD_LO_PATH, _REV_LO_PATH)[strand_type] self.is3p = True if is_fwd else False self.enter_pos = _FWD_LO_PTS[0][-1] if is_fwd else _REV_LO_PTS[0][ -1] self.exit_pos = _FWD_LO_PTS[0][-1] if is_fwd else _REV_LO_PTS[0][-1] elif is_high: path = (_FWD_HI_PATH, _REV_HI_PATH)[strand_type] self.is3p = False if is_fwd else True self.enter_pos = _FWD_HI_PTS[0][-1] if is_fwd else _REV_HI_PTS[0][ -1] self.exit_pos = _FWD_HI_PTS[0][-1] if is_fwd else _REV_HI_PTS[0][-1] else: # print("unpaired PreXoverItem at {}[{}]".format(self._id_num, self.idx), self.nearby_idxs) return False self._tick_marks.setPen( getPenObj(self.color, styles.PREXOVER_STROKE_WIDTH, capstyle=Qt.FlatCap, joinstyle=Qt.RoundJoin)) self._tick_marks.setPath(path) self._tick_marks.show() return True # end def ### ACCESSORS ### def color(self) -> str: """The PreXoverItem's color, derived from the associated strand's oligo. Returns: str: color in hex code """ return self.color def getInfo(self) -> ABInfoT: """ Returns: Tuple: (from_id_num, is_fwd, from_index, to_vh_id_num) """ return (self._id_num, self.is_fwd, self.idx, self.to_vh_id_num) def destroyItem(self): """Removes animation adapter, label, bond_item, and this item from scene. """ scene = self.scene() self.adapter.destroyItem() if scene: scene.removeItem(self._label) self._label = None scene.removeItem(self._bond_item) self._bond_item = None self.adapter.resetAnimations() self.adapter = None scene.removeItem(self) # end defS ### EVENT HANDLERS ### def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): """Only ``if enableActive(True)`` is called hover and key events disabled by default Args: event: the hover event """ if self._getActiveTool().methodPrefix() != "selectTool": return self.setFocus(Qt.MouseFocusReason) self.prexoveritem_manager.updateModelActiveBaseInfo(self.getInfo()) self.setActiveHovered(True) status_string = "%d[%d]" % (self._id_num, self.idx) self.parentItem().window().statusBar().showMessage(status_string) return QGraphicsItem.hoverEnterEvent(self, event) # end def def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): """Summary Args: event (QGraphicsSceneHoverEvent): the hover event """ self.prexoveritem_manager.updateModelActiveBaseInfo(None) self.setActiveHovered(False) self.clearFocus() self.parentItem().window().statusBar().showMessage("") return QGraphicsItem.hoverLeaveEvent(self, event) # end def def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """TODO: NEED TO ADD FILTER FOR A CLICK ON THE 3' MOST END OF THE XOVER TO DISALLOW OR HANDLE DIFFERENTLY """ viewroot = self.parentItem().viewroot() current_filter_set = viewroot.selectionFilterSet() if (self._getActiveTool().methodPrefix() != "selectTool" or (self.FILTER_NAME not in current_filter_set)): return part = self._model_part is_fwd = self.is_fwd if self.is3p: strand5p = part.getStrand(is_fwd, self._id_num, self.idx) strand3p = part.getStrand(not is_fwd, self.to_vh_id_num, self.idx) else: strand5p = part.getStrand(not is_fwd, self.to_vh_id_num, self.idx) strand3p = part.getStrand(is_fwd, self._id_num, self.idx) if strand5p is None or strand3p is None: return # print(strand3p, strand5p) part.createXover(strand5p, self.idx, strand3p, self.idx) nkey = (self.to_vh_id_num, not is_fwd, self.idx) npxi = self.prexoveritem_manager.neighbor_prexover_items.get( nkey, None) if npxi: npxi.shutdown() self.shutdown() part.setActiveVirtualHelix(self._id_num, is_fwd, self.idx) # self.prexoveritem_manager.handlePreXoverClick(self) def keyPressEvent(self, event: QKeyEvent): """ Args: event: Description """ self.prexoveritem_manager.handlePreXoverKeyPress(event.key()) # end def ### PUBLIC SUPPORT METHODS ### def setLabel(self, text: str = None, outline: bool = False): """Summary Args: text: Default is ``None`` outline: Default is ``False`` """ if text: self._label.setTextAndStyle(text=text, outline=outline) self._label.show() else: self._label.hide() # end def def animate(self, item: QGraphicsItem, property_name: str, duration: int, start_value, end_value): """ Args: item: Description property_name: Description duration: Description start_value (QVariant): Description end_value (QVariant): Description """ b_name = property_name.encode('ascii') anim = item.adapter.getRef(property_name) if anim is None: anim = QPropertyAnimation(item.adapter, b_name) item.adapter.saveRef(property_name, anim) anim.setDuration(duration) anim.setStartValue(start_value) anim.setEndValue(end_value) anim.start() # end def def setActiveHovered(self, is_active: bool): """Rotate phosphate Triangle if `self.to_vh_id_num` is not `None` Args: is_active: whether or not the PreXoverItem is parented to the active VirtualHelixItem """ pass # end def def enableActive(self, is_active: bool, to_vh_id_num: int = None): """Call on PreXoverItems created on the active VirtualHelixItem Args: is_active: Description to_vh_id_num: Default is ``None`` """ if is_active: self.to_vh_id_num = to_vh_id_num self.setAcceptHoverEvents(True) if to_vh_id_num is None: self.setLabel(text=None) else: self.setLabel(text=str(to_vh_id_num)) else: self.setBrush(getNoBrush()) self.setAcceptHoverEvents(False) def activateNeighbor(self, active_prexoveritem: 'PreXoverItem', shortcut: str = None): """Draws a quad line starting from the item5p to the item3p. To be called with whatever the active_prexoveritem is for the parts `active_base`. Args: active_prexoveritem: Description shortcut: Default is None """ if self._getActiveTool().methodPrefix() != "selectTool": return if self.is3p and not active_prexoveritem.is3p: item5p = active_prexoveritem item3p = self elif not self.is3p and active_prexoveritem.is3p: item5p = self item3p = active_prexoveritem else: return same_parity = self.is_fwd == active_prexoveritem.is_fwd p1 = item5p._tick_marks.scenePos() + item5p.exit_pos p2 = item3p._tick_marks.scenePos() + item3p.exit_pos c1 = QPointF() # case 1: same parity if same_parity: dy = abs(p2.y() - p1.y()) c1.setX(p1.x() + _X_SCALE * dy) c1.setY(0.5 * (p1.y() + p2.y())) # case 2: different parity else: if item3p.is_fwd: c1.setX(p1.x() - _X_SCALE * abs(p2.y() - p1.y())) else: c1.setX(p1.x() + _X_SCALE * abs(p2.y() - p1.y())) c1.setY(0.5 * (p1.y() + p2.y())) pp = QPainterPath() pp.moveTo(self._tick_marks.mapFromScene(p1)) pp.quadTo(self._tick_marks.mapFromScene(c1), self._tick_marks.mapFromScene(p2)) # pp.cubicTo(c1, c2, self._tick_marks.mapFromScene(p2)) self._bond_item.setPath(pp) self._bond_item.show() # end def def deactivateNeighbor(self): """Summary """ if self.isVisible(): self._bond_item.hide() self.setLabel(text=self._label_txt)
class ForcedXoverNode3(QGraphicsRectItem): """ This is a QGraphicsRectItem to allow actions and also a QGraphicsSimpleTextItem to allow a label to be drawn """ def __init__(self, virtual_helix_item, xover_item, strand3p, idx): super(ForcedXoverNode3, self).__init__(virtual_helix_item) self._vhi = virtual_helix_item self._xover_item = xover_item self._idx = idx self._is_on_top = virtual_helix_item.isStrandOnTop(strand3p) self._is_drawn_5_to_3 = strand3p.strandSet().isDrawn5to3() self._strand_type = strand3p.strandSet().strandType() self._partner_virtual_helix = virtual_helix_item self._blank_thing = QGraphicsRectItem(_blankRect, self) self._blank_thing.setBrush(QBrush(Qt.white)) self._path_thing = QGraphicsPathItem(self) self.configurePath() self.setPen(_NO_PEN) self._label = None self.setPen(_NO_PEN) self.setBrush(_NO_BRUSH) self.setRect(_rect) self.setZValue(styles.ZENDPOINTITEM + 1) # end def def updateForFloatFromVHI(self, virtual_helix_item, strand_type, idx_x, idx_y): """ """ self._vhi = virtual_helix_item self.setParentItem(virtual_helix_item) self._strand_type = strand_type self._idx = idx_x self._is_on_top = self._is_drawn_5_to_3 = True if idx_y == 0 else False self.updatePositionAndAppearance(is_from_strand=False) # end def def updateForFloatFromStrand(self, virtual_helix_item, strand3p, idx): """ """ self._vhi = virtual_helix_item self._strand = strand3p self.setParentItem(virtual_helix_item) self._idx = idx self._is_on_top = virtual_helix_item.isStrandOnTop(strand3p) self._is_drawn_5_to_3 = strand3p.strandSet().isDrawn5to3() self._strand_type = strand3p.strandSet().strandType() self.updatePositionAndAppearance() # end def def strandType(self): return self._strand_type # end def def configurePath(self): self._path_thing.setBrush(QBrush(styles.RED_STROKE)) path = PPR3 if self._is_drawn_5_to_3 else PPL3 offset = -_BASE_WIDTH if self._is_drawn_5_to_3 else _BASE_WIDTH self._path_thing.setPath(path) self._path_thing.setPos(offset, 0) offset = -_BASE_WIDTH if self._is_drawn_5_to_3 else 0 self._blank_thing.setPos(offset, 0) self._blank_thing.show() self._path_thing.show() # end def def refreshXover(self): self._xover_item.refreshXover() # end def def setPartnerVirtualHelix(self, virtual_helix_item): self._partner_virtual_helix = virtual_helix_item # end def def idx(self): return self._idx # end def def virtualHelixItem(self): return self._vhi # end def def point(self): return self._vhi.upperLeftCornerOfBaseType(self._idx, self._strand_type) # end def def floatPoint(self): pt = self.pos() return pt.x(), pt.y() # end def def isOnTop(self): return self._is_on_top # end def def isDrawn5to3(self): return self._is_drawn_5_to_3 # end def def updatePositionAndAppearance(self, is_from_strand=True): """ Sets position by asking the VirtualHelixItem Sets appearance by choosing among pre-defined painterpaths (from normalstrandgraphicsitem) depending on drawing direction. """ self.setPos(*self.point()) n5 = self._xover_item._node5 if is_from_strand: from_strand, from_idx = (n5._strand, n5._idx) if n5 != self else (None, None) if self._strand.canInstallXoverAt(self._idx, from_strand, from_idx): self.configurePath() # We can only expose a 5' end. But on which side? is_left = True if self._is_drawn_5_to_3 else False self._updateLabel(is_left) else: self.hideItems() else: self.hideItems() # end def def updateConnectivity(self): is_left = True if self._is_drawn_5_to_3 else False self._updateLabel(is_left) # end def def remove(self): """ Clean up this joint """ scene = self.scene() scene.removeItem(self._label) self._label = None scene.removeItem(self._path_thing) self._path_thing = None scene.removeItem(self._blank_thing) self._blank_thing = None scene.removeItem(self) # end def def _updateLabel(self, is_left): """ Called by updatePositionAndAppearance during init, or later by updateConnectivity. Updates drawing and position of the label. """ lbl = self._label if self._idx != None: bw = _BASE_WIDTH num = self._partner_virtual_helix.number() tBR = _FM.tightBoundingRect(str(num)) half_label_h = tBR.height() / 2.0 half_label_w = tBR.width() / 2.0 # determine x and y positions label_x = bw / 2.0 - half_label_w if self._is_on_top: label_y = -0.25 * half_label_h - 0.5 - 0.5 * bw else: label_y = 2 * half_label_h + 0.5 + 0.5 * bw # adjust x for left vs right label_x_offset = 0.25 * bw if is_left else -0.25 * bw label_x += label_x_offset # adjust x for numeral 1 if num == 1: label_x -= half_label_w / 2.0 # create text item if lbl == None: lbl = QGraphicsSimpleTextItem(str(num), self) lbl.setPos(label_x, label_y) lbl.setBrush(_ENAB_BRUSH) lbl.setFont(_TO_HELIX_NUM_FONT) self._label = lbl lbl.setText(str(self._partner_virtual_helix.number())) lbl.show() # end if # end def def hideItems(self): if self._label: self._label.hide() if self._blank_thing: self._path_thing.hide() if self._blank_thing: self._blank_thing.hide()
class CharItem(QGraphicsRectItem): """ This item represents character item The purpose of the class is to draw a character, create a matrix of rectangles and resolve in which rectangles the character passes The class allow the following operations -# Drawing a character using the mouse events: -# Start by the mouse press event -# Continues by the mouse move event -# The character is stored in QGraphicsPathItem -# Transform the character to occupy the whole item's space -# Set operation : resolving the Occupied matrix which tell on which rectangle the character passes -# Reset operation : reverse the character transform so it is possible to continue drawing the character -# Save operation : To a QDataStream -# Load operation : From a QDataStream The graphical view of the class is composed from: -# This class which inherits from QGraphicsRectItem and holds : -# A QGraphicsPathItem : representing the character -# A QGraphicsPathItem : representing the occupied rectangles -# A QGraphicsPathItem : representing the unoccupied rectangles """ def __init__(self, rect: QRectF, pos: QPointF, viewIndex: int=-1): """ CharItem constructor Args: rect (QRectF) : The rectangle that the character should fill pos (QPointF) : The position of the item within the parent viewIndex (int) : The index of the item in case it is presented in multi character presentation """ super(CharItem, self).__init__(rect) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPresentationPrms() self.occupied = [[False for idx in range(self.netCols)] for idx in range(self.netRows)] self.charPath = None self.wasSetted = False self.occupiedPathItem = None self.unoccupiedPathItem = None self.dirty = False self.viewIndex = viewIndex self.filename = "" self.boundaries = rect self.dx = 1 self.dy = 1 self.posInParent = pos self.setPos(self.posInParent) def setPresentationPrms(self): """ Setting the presentation prms The reason the there is a duplicate set of presentation parameters is that it allows changing the presentation parameters for one character (like in the select option """ self.netColor = netColor self.netThickness = netThickness self.occupyColor = occupyColor self.unOccupyColor = unOccupyColor self.shapeColor = shapeColor self.shapeLineThickness = shapeLineThickness self.selectedOccupiedColor = selectedOccupiedColor self.selectedShapeColor = selectedShapeColor self.netRows = netRows self.netCols = netCols def setNetBoxDimensions(self, rect: QRectF): """ Set net box dimensions The net box is the rectangle that compose the network drawn to show the occupy matrix """ self.netRectHeight = rect.height() / self.netRows self.netRectWidth = rect.width() / self.netCols self.left = rect.left() self.top = rect.top() def netRect(self, row_idx: int, col_idx: int) -> QRectF: """ Set net rect The net box is the rectangle that compose the network drawn to show the occupy matrix Args: row_idx (int) : The row of the network rectangle col_idx (int) : The col of the network rectangle Returns: QRectF : The rectangle """ return QRectF(self.left + col_idx * self.netRectWidth, self.top + row_idx * self.netRectHeight, self.netRectWidth, self.netRectHeight) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse move event : continue draw a line This event is activated when the mouse is pressed and moves The methods draws the line in 2 conditions: -# The item is not part of multi character presentation -# A character path was initiated (using the mouse press event) Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: if self.charPath is not None: point = event.scenePos() path = self.charPath.path() path.lineTo(point) self.charPath.setPath(path) self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse Press Event : Start a new line / Select the character If the character is part of multi character presentation activate the character selection If the character is in single character presentation Start a new line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: self.startLine(event) else: self.setSelected() def startLine(self, event: QGraphicsSceneMouseEvent): """ Start drawing a line When the mouse button is pressed and we are in single character dialog this method is activated to start drowning a line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ # There are 2 modes for the presentation: # Original mode where the character is it's original size # After setting mode when the set was done and the character fullfill all # the item's space # Drawing can be done only in original mode if self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was already setted use revert setting") return # If this is the first start of a line - generate the QPainterPath and QGraphicsPathItem if self.charPath is None: self.initCharPath() # Move to the mouse position point = event.scenePos() path = self.charPath.path() path.moveTo(point) self.charPath.setPath(path) self.dirty = True def initCharPath(self): """ Init the item that holds the character There is one path item that holds the character This method is activated by start line if the char item was not created to create the new and only one """ self.dirty = True self.charPath = QGraphicsPathItem(self) self.charPath.setPen(QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.charPath.setZValue(1) self.charPath.originalPos = self.charPath.pos() self.charPath.setPath(QPainterPath()) def setSelected(self): """ Set the item a selected item This method is activated when the mouse button is presses and the item is part of multi character presentation """ # Set the colors of the item self.occupiedPathItem.setBrush(QBrush(QColor(self.selectedOccupiedColor))) self.charPath.setPen(QPen(QColor(self.selectedShapeColor), self.shapeLineThickness)) self.update() # Report to the parent item about the selection self.parentItem().setSelected(self.viewIndex) def resetSelected(self): """ Set the colors of the item to not selected """ self.occupiedPathItem.setBrush(QBrush(QColor(self.occupyColor))) self.charPath.setPen(QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.update() def set(self): """ Calculate the occupied matrix and present the results This method does the following: -# Fill the occupied matrix -# Generate the occupied and unoccupied pathes items -# Transform the char path to fit to the item's boundaries """ # If there is no shape drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is in setted mode - return if self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was already setted use revert setting") return # fill the occupied matrix with the data before the scaling self.fillOccupied() self.setNetBoxDimensions(self.boundingRect()) self.createNetPaths() # update the transform - change the dimensions and location # only on the first time self.transformCharPath() self.wasSetted = True # update the presentation self.update() def revertTransform(self): """ Change from Setted mode to drawing mode The drawing mode is the mode where the character can be drawn -# Restore the original size of the character (Reset the transform of the char item) -# Restor the char path item's position to the original one (saved when created) -# Empty the occupiedPath and the unoccupiedPath """ # If there is no character drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is already in drawing mode - return if not self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was not setted use set button") return # The char path item transform = self.charPath.transform() transform.reset() # The self.dx and self.dy are the scale parameters created when the item # begins and they are the scale parameters that transform it to the boundaries # given by the parent item transform.scale(self.dx, self.dy) self.charPath.setTransform(transform) self.charPath.setPos(self.charPath.originalPos) # Empty the network pathes self.occupiedPathItem.setPath(QPainterPath()) self.unoccupiedPathItem.setPath(QPainterPath()) self.wasSetted = False def transformCharPath(self): """ Transform char path when the item is setted This method does the following -# scale the char path to the size of the item -# calculate the new position of the char path so that it will be placed at the top left corner of the item """ dx = self.boundingRect().width() / self.charPath.boundingRect().width() dy = self.boundingRect().height() / self.charPath.boundingRect().height() transform = self.charPath.transform() transform.reset() transform.scale(dx, dy) self.charPath.setTransform(transform) # Move the shape to the origin moveX = -(self.charPath.boundingRect().left() - self.boundingRect().left()) * dx moveY = -(self.charPath.boundingRect().top() - self.boundingRect().top()) * dy self.charPath.setX(self.charPath.x() + moveX) self.charPath.setY(self.charPath.y() + moveY) def fillOccupied(self): """ Fill the occupied matrix The algorithm of filling the occupied matrix is -# Scanning the char path -# For each point decide on where row and column of the net -# Set the occupies matrix for this column and row to True """ for idx in range(100): point = self.charPath.path().pointAtPercent(idx / 100.) row_idx, col_idx = self.calcRowCol(point) self.occupied[row_idx][col_idx] = True def calcRowCol(self, point: QPointF): """ Calculate the network row and column that a point is int calc the row and column indexes of a point The following is the algorithm: 1. Find the distance between the point and the left (or top) 2. Divide the distance with the width of path to find the relative position 3. Multipile this relative position with the number of rows/cols 4. Convert the result to int to find the indexes 5. If the index is the number of row/col reduce the index (This is for the case the the point is on the boundary and in this case the relative position is 1 which will cause the indexes to be the number of rows/cols - out of the matrix indexes) Args: point (QPointF) : The point to resolve Returns: int : The network row that the point is in int : The network column that the point is in """ partialX = (point.x() - self.charPath.boundingRect().left()) / self.charPath.boundingRect().width() partialY = (point.y() - self.charPath.boundingRect().top()) / self.charPath.boundingRect().height() col_idx = int(partialX * self.netCols) row_idx = int(partialY * self.netRows) if row_idx == self.netRows: row_idx -= 1 if col_idx == self.netCols: col_idx -= 1 return row_idx, col_idx def createNetPaths(self): """ Create the network pathes This method creates 2 network pathes items one for holding the occupied rectangles and one to hold the unoccupied rectangles """ # Generate 2 QPainterPath occupiedPath = QPainterPath() unoccupiedPath = QPainterPath() # For each entry in occupied matrix : # Add a rectangle to the appropriate path according the entry value for row_idx in range(self.netRows): for col_idx in range(self.netCols): if self.occupied[row_idx][col_idx]: occupiedPath.addRect(self.netRect(row_idx, col_idx)) else: unoccupiedPath.addRect(self.netRect(row_idx, col_idx)) # Create the QGraphicsPathItems that will hold the path self.createNetPath(self.occupyColor, occupiedPath, True) self.createNetPath(self.unOccupyColor, unoccupiedPath, False) def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen(QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0) def save(self, stream: QDataStream, filename: str): """ Save the item to QDataStream Args: stream (QDataStream) : The data stream to write the item to filename (str) : The filename (for documenting purposes) """ # The item position stream << self.pos() # The dimensions stream << self.rect() # The presentation parameters stream.writeQString(self.netColor) stream.writeQString(self.occupyColor) stream.writeQString(self.unOccupyColor) stream.writeQString(self.shapeColor) stream.writeInt16(self.shapeLineThickness) stream.writeInt16(self.netRows) stream.writeInt16(self.netRows) # The items paths stream << self.charPath.path() self.dirty = False self.filename = filename def load(self, stream, filename): """ Loads the item from QDataStream Args: stream (QDataStream) : The data stream to read the item from filename (str) : The filename (for documenting purposes) """ # read the pos pos = QPointF() stream >> pos self.setPos(pos) # read the dimensions rect = QRectF() stream >> rect self.setRect(rect) # The presentation parameters self.netColor = stream.readQString() self.occupyColor = stream.readQString() self.unOccupyColor = stream.readQString() self.shapeColor = stream.readQString() self.shapeLineThickness = stream.readInt16() self.netRows = stream.readInt16() self.netRows = stream.readInt16() # read the paths self.initCharPath() path = self.charPath.path() stream >> path self.charPath.setPath(path) # Fit the item to the boundaries and position given by the item's parent self.fitToBoundaries() # The presentation of the item is in setted mode so we activate the set method self.wasSetted = False self.set() self.dirty = False self.filename = filename def fitToBoundaries(self): """ Fit the item to the boundaries and position given by it's parent This method was made to support the change of the character boundaries and that the char can be presented in different boundaries and position """ self.setPos(self.posInParent) self.dx = self.boundaries.width() / self.rect().width() self.dy = self.boundaries.height() / self.rect().height() transform = self.transform() transform.scale(self.dx, self.dy) self.setTransform(transform)
class ReacItem(KineticsDisplayItem): defaultWidth = 30 defaultHeight = 30 defaultPenWidth = 2 name = constants.ITEM def __init__(self, *args, **kwargs): KineticsDisplayItem.__init__(self, *args, **kwargs) points = [ QtCore.QPointF(ReacItem.defaultWidth / 4, 0), QtCore.QPointF(0, ReacItem.defaultHeight / 4), QtCore.QPointF(ReacItem.defaultWidth, ReacItem.defaultHeight / 4), QtCore.QPointF(3 * ReacItem.defaultWidth / 4, ReacItem.defaultHeight / 2) ] path = QtGui.QPainterPath() path.moveTo(points[0]) for p in points[1:]: path.lineTo(p) path.moveTo(p) self.gobj = QGraphicsPathItem(path, self) self.gobj.setPen( QtGui.QPen(QtCore.Qt.black, 2, Qt.Qt.SolidLine, Qt.Qt.RoundCap, Qt.Qt.RoundJoin)) self.gobj.mobj = self.mobj self._Kf = self.gobj.mobj.Kf self._Kb = self.gobj.mobj.Kb doc = "Kf\t: " + str(self._Kf) + "\nKb\t: " + str(self._Kb) self.gobj.setToolTip(doc) def updateValue(self, gobj): self._gobj = gobj if (isinstance(self._gobj, moose.ReacBase)): self._Kf = self._gobj.Kf self._Kb = self._gobj.Kb doc = "Kf\t: " + str(self._Kf) + "\nKb\t: " + str(self._Kb) self.gobj.setToolTip(doc) def refresh(self, scale): defaultWidth = ReacItem.defaultWidth * scale defaultHeight = ReacItem.defaultHeight * scale points = [ QtCore.QPointF(defaultWidth / 4, 0), QtCore.QPointF(0, defaultHeight / 4), QtCore.QPointF(defaultWidth, defaultHeight / 4), QtCore.QPointF(3 * defaultWidth / 4, defaultHeight / 2) ] path = QtGui.QPainterPath() path.moveTo(points[0]) for p in points[1:]: path.lineTo(p) path.moveTo(p) self.gobj.setPath(path) ReacPen = self.gobj.pen() defaultpenwidth = ReacItem.defaultPenWidth reacWidth = defaultpenwidth * scale ReacPen.setWidth(reacWidth) self.gobj.setPen(ReacPen) def setDisplayProperties(self, x, y, textcolor, bgcolor): """Set the display properties of this item.""" self.setGeometry(x, y, self.gobj.boundingRect().width(), self.gobj.boundingRect().height())
class PreXoverItem(QGraphicsRectItem): """A PreXoverItem exists between a single 'from' VirtualHelixItem index and zero or more 'to' VirtualHelixItem Indices Attributes: adapter (TYPE): Description idx (int): the base index within the virtual helix is_fwd (TYPE): Description prexoveritem_manager (TYPE): Description to_vh_id_num (TYPE): Description """ def __init__(self, from_virtual_helix_item, is_fwd, from_index, nearby_idxs, to_vh_id_num, prexoveritem_manager): """Summary Args: from_virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): Description is_fwd (TYPE): Description from_index (TYPE): Description to_vh_id_num (TYPE): Description prexoveritem_manager (TYPE): Description """ super(QGraphicsRectItem, self).__init__(BASE_RECT, from_virtual_helix_item) self.adapter = PropertyWrapperObject(self) self._tick_marks = QGraphicsPathItem(self) self._tick_marks.setAcceptHoverEvents(True) self._bond_item = QGraphicsPathItem(self) self._bond_item.hide() self._label = PreXoverLabel(is_fwd, self) self._path = QGraphicsPathItem() self.setZValue(styles.ZPREXOVERITEM) self.setPen(getNoPen()) self.resetItem(from_virtual_helix_item, is_fwd, from_index, nearby_idxs, to_vh_id_num, prexoveritem_manager) # end def def shutdown(self): """Summary Returns: TYPE: Description """ self.setBrush(getNoBrush()) self.to_vh_id_num = None self.adapter.resetAnimations() self.setAcceptHoverEvents(False) self.hide() # end def def resetItem(self, from_virtual_helix_item, is_fwd, from_index, nearby_idxs, to_vh_id_num, prexoveritem_manager): """Update this pooled PreXoverItem with current info. Called by PreXoverManager. Args: from_virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): the associated vh_item is_fwd (bool): True if associated with fwd strand, False if rev strand from_index (int): idx of associated vh to_vh_id_num (int): id_num of the other vh prexoveritem_manager (cadnano.views.pathview.prexoermanager.PreXoverManager): the manager """ self.setParentItem(from_virtual_helix_item) self.resetTransform() self._id_num = from_virtual_helix_item.idNum() self._model_vh = from_virtual_helix_item.cnModel() self.idx = from_index self.is_low = False self.is_high = False self.nearby_idxs = nearby_idxs self.is_fwd = is_fwd self.color = None self.is3p = None self.enter_pos = None self.exit_pos = None self.to_vh_id_num = to_vh_id_num self.prexoveritem_manager = prexoveritem_manager # todo: check here if xover present and disable result = self.setPathAppearance(from_virtual_helix_item) if result: self.setBrush(getNoBrush()) if is_fwd: self.setPos(from_index*BASE_WIDTH, -BASE_WIDTH) else: self.setPos(from_index*BASE_WIDTH, 2*BASE_WIDTH) self.show() # label self._label_txt = lbt = None if to_vh_id_num is None else str(to_vh_id_num) self.setLabel(text=lbt) self._label.resetItem(is_fwd, self.color) # bond line bonditem = self._bond_item bonditem.setPen(getPenObj(self.color, styles.PREXOVER_STROKE_WIDTH)) bonditem.hide() # end def def setPathAppearance(self, from_virtual_helix_item): """ Sets the PainterPath according to the index (low = Left, high = Right) and strand position (top = Up, bottom = Down). """ part = self._model_vh.part() idx = self.idx is_fwd = self.is_fwd id_num = self._id_num strand_type = StrandType.FWD if is_fwd else StrandType.REV # relative position info bpr = from_virtual_helix_item.getProperty('bases_per_repeat') self.is_low = is_low = idx+1 in self.nearby_idxs or (idx+1) % bpr in self.nearby_idxs self.is_high = is_high = idx-1 in self.nearby_idxs or (idx-1) % bpr in self.nearby_idxs # check strand for xover and color if part.hasStrandAtIdx(id_num, idx)[strand_type]: strand = part.getStrand(self.is_fwd, id_num, idx) if strand.hasXoverAt(idx): return False self.color = strand.getColor() if strand is not None else EMPTY_COL else: self.color = EMPTY_COL if is_low and is_high: print("dual xover") path = (_FWD_DUAL_PATH, _REV_DUAL_PATH)[strand_type] elif is_low: path = (_FWD_LO_PATH, _REV_LO_PATH)[strand_type] self.is3p = True if is_fwd else False self.enter_pos = _FWD_LO_PTS[0][-1] if is_fwd else _REV_LO_PTS[0][-1] self.exit_pos = _FWD_LO_PTS[0][-1] if is_fwd else _REV_LO_PTS[0][-1] elif is_high: path = (_FWD_HI_PATH, _REV_HI_PATH)[strand_type] self.is3p = False if is_fwd else True self.enter_pos = _FWD_HI_PTS[0][-1] if is_fwd else _REV_HI_PTS[0][-1] self.exit_pos = _FWD_HI_PTS[0][-1] if is_fwd else _REV_HI_PTS[0][-1] else: # print("unpaired PreXoverItem at {}[{}]".format(self._id_num, self.idx), self.nearby_idxs) return False self._tick_marks.setPen(getPenObj(self.color, styles.PREXOVER_STROKE_WIDTH, capstyle=Qt.FlatCap, joinstyle=Qt.RoundJoin)) self._tick_marks.setPath(path) self._tick_marks.show() return True # end def ### ACCESSORS ### def color(self): """The PreXoverItem's color, derived from the associated strand's oligo. Returns: str: color in hex code """ return self.color def getInfo(self): """ Returns: Tuple: (from_id_num, is_fwd, from_index, to_vh_id_num) """ return (self._id_num, self.is_fwd, self.idx, self.to_vh_id_num) def remove(self): """Removes animation adapter, label, bond_item, and this item from scene. """ scene = self.scene() self.adapter.destroy() if scene: scene.removeItem(self._label) self._label = None scene.removeItem(self._bond_item) self._bond_item = None self.adapter.resetAnimations() self.adapter = None scene.removeItem(self) # end defS ### EVENT HANDLERS ### def hoverEnterEvent(self, event): """Only if enableActive(True) is called hover and key events disabled by default Args: event (QGraphicsSceneHoverEvent): the hover event """ self.setFocus(Qt.MouseFocusReason) self.prexoveritem_manager.updateModelActiveBaseInfo(self.getInfo()) self.setActiveHovered(True) status_string = "%d[%d]" % (self._id_num, self.idx) self.parentItem().window().statusBar().showMessage(status_string) # end def def hoverLeaveEvent(self, event): """Summary Args: event (QGraphicsSceneHoverEvent): the hover event Returns: TYPE: Description """ self.prexoveritem_manager.updateModelActiveBaseInfo(None) self.setActiveHovered(False) self.clearFocus() self.parentItem().window().statusBar().showMessage("") # end def def mousePressEvent(self, event): part = self._model_vh.part() is_fwd = self.is_fwd if self.is3p: strand5p = part.getStrand(is_fwd, self._id_num, self.idx) strand3p = part.getStrand(not is_fwd, self.to_vh_id_num, self.idx) else: strand5p = part.getStrand(not is_fwd, self.to_vh_id_num, self.idx) strand3p = part.getStrand(is_fwd, self._id_num, self.idx) if strand5p is None or strand3p is None: return part.createXover(strand5p, self.idx, strand3p, self.idx) nkey = (self.to_vh_id_num, not is_fwd, self.idx) npxi = self.prexoveritem_manager.neighbor_prexover_items.get(nkey, None) if npxi: npxi.shutdown() self.shutdown() part.setActiveVirtualHelix(self._id_num, is_fwd, self.idx) # self.prexoveritem_manager.handlePreXoverClick(self) def keyPressEvent(self, event): """Summary Args: event (TYPE): Description Returns: TYPE: Description """ self.prexoveritem_manager.handlePreXoverKeyPress(event.key()) # end def ### PUBLIC SUPPORT METHODS ### def setLabel(self, text=None, outline=False): """Summary Args: text (None, optional): Description outline (bool, optional): Description Returns: TYPE: Description """ if text: self._label.setTextAndStyle(text=text, outline=outline) self._label.show() else: self._label.hide() # end def def animate(self, item, property_name, duration, start_value, end_value): """Summary Args: item (TYPE): Description property_name (TYPE): Description duration (TYPE): Description start_value (TYPE): Description end_value (TYPE): Description Returns: TYPE: Description """ b_name = property_name.encode('ascii') anim = item.adapter.getRef(property_name) if anim is None: anim = QPropertyAnimation(item.adapter, b_name) item.adapter.saveRef(property_name, anim) anim.setDuration(duration) anim.setStartValue(start_value) anim.setEndValue(end_value) anim.start() # end def def setActiveHovered(self, is_active): """Rotate phosphate Triangle if `self.to_vh_id_num` is not `None` Args: is_active (bool): whether or not the PreXoverItem is parented to the active VirtualHelixItem """ pass # end def def enableActive(self, is_active, to_vh_id_num=None): """Call on PreXoverItems created on the active VirtualHelixItem Args: is_active (TYPE): Description to_vh_id_num (None, optional): Description """ if is_active: self.to_vh_id_num = to_vh_id_num self.setAcceptHoverEvents(True) if to_vh_id_num is None: self.setLabel(text=None) else: self.setLabel(text=str(to_vh_id_num)) else: self.setBrush(getNoBrush()) self.setAcceptHoverEvents(False) def activateNeighbor(self, active_prexoveritem, shortcut=None): """ Draws a quad line starting from the item5p to the item3p. To be called with whatever the active_prexoveritem is for the parts `active_base`. Args: active_prexoveritem (TYPE): Description shortcut (None, optional): Description """ if self.is3p and not active_prexoveritem.is3p: item5p = active_prexoveritem item3p = self elif not self.is3p and active_prexoveritem.is3p: item5p = self item3p = active_prexoveritem else: return same_parity = self.is_fwd == active_prexoveritem.is_fwd p1 = item5p._tick_marks.scenePos() + item5p.exit_pos p2 = item3p._tick_marks.scenePos() + item3p.exit_pos c1 = QPointF() # case 1: same parity if same_parity: dy = abs(p2.y() - p1.y()) c1.setX(p1.x() + _X_SCALE * dy) c1.setY(0.5 * (p1.y() + p2.y())) # case 2: different parity else: if item3p.is_fwd: c1.setX(p1.x() - _X_SCALE * abs(p2.y() - p1.y())) else: c1.setX(p1.x() + _X_SCALE * abs(p2.y() - p1.y())) c1.setY(0.5 * (p1.y() + p2.y())) pp = QPainterPath() pp.moveTo(self._tick_marks.mapFromScene(p1)) pp.quadTo(self._tick_marks.mapFromScene(c1), self._tick_marks.mapFromScene(p2)) # pp.cubicTo(c1, c2, self._tick_marks.mapFromScene(p2)) self._bond_item.setPath(pp) self._bond_item.show() # end def def deactivateNeighbor(self): """Summary Returns: TYPE: Description """ if self.isVisible(): self._bond_item.hide() self.setLabel(text=self._label_txt)
class ForcedXoverNode3(QGraphicsRectItem): """ This is a QGraphicsRectItem to allow actions and also a QGraphicsSimpleTextItem to allow a label to be drawn Attributes: is_forward (TYPE): Description """ def __init__(self, virtual_helix_item, xover_item, strand3p, idx): """Summary Args: virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): from vhi xover_item (TYPE): Description strand3p (Strand): reference to the 3' strand idx (int): the base index within the virtual helix """ super(ForcedXoverNode3, self).__init__(virtual_helix_item) self._vhi = virtual_helix_item self._xover_item = xover_item self._idx = idx self.is_forward = strand3p.strandSet().isForward() self._is_on_top = self.is_forward self._partner_virtual_helix = virtual_helix_item self._blank_thing = QGraphicsRectItem(_blankRect, self) self._blank_thing.setBrush(QBrush(Qt.white)) self._path_thing = QGraphicsPathItem(self) self.configurePath() self._label = None self.setPen(_NO_PEN) self.setBrush(_NO_BRUSH) self.setRect(_rect) self.setZValue(styles.ZENDPOINTITEM + 1) # end def def updateForFloatFromVHI(self, virtual_helix_item, is_forward, idx_x, idx_y): """ Args: virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): Description is_forward (TYPE): Description idx_x (TYPE): Description idx_y (TYPE): Description """ self._vhi = virtual_helix_item self.setParentItem(virtual_helix_item) self._idx = idx_x self._is_on_top = self.is_forward = True if is_forward else False self.updatePositionAndAppearance(is_from_strand=False) # end def def updateForFloatFromStrand(self, virtual_helix_item, strand3p, idx): """ Args: virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): Description strand3p (Strand): reference to the 3' strand idx (int): the base index within the virtual helix """ self._vhi = virtual_helix_item self._strand = strand3p self.setParentItem(virtual_helix_item) self._idx = idx self._is_on_top = self.is_forward = strand3p.strandSet().isForward() self.updatePositionAndAppearance() # end def def configurePath(self): """Summary Returns: TYPE: Description """ self._path_thing.setBrush(getBrushObj(_PENCIL_COLOR)) path = PPR3 if self.is_forward else PPL3 offset = -_BASE_WIDTH if self.is_forward else _BASE_WIDTH self._path_thing.setPath(path) self._path_thing.setPos(offset, 0) offset = -_BASE_WIDTH if self.is_forward else 0 self._blank_thing.setPos(offset, 0) self._blank_thing.show() self._path_thing.show() # end def def refreshXover(self): """Summary Returns: TYPE: Description """ self._xover_item.refreshXover() # end def def setPartnerVirtualHelix(self, virtual_helix_item): """Summary Args: virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): Description Returns: TYPE: Description """ self._partner_virtual_helix = virtual_helix_item # end def def idx(self): """Summary Returns: TYPE: Description """ return self._idx # end def def virtualHelixItem(self): """Summary Returns: TYPE: Description """ return self._vhi # end def def point(self): """Summary Returns: TYPE: Description """ return self._vhi.upperLeftCornerOfBaseType(self._idx, self.is_forward) # end def def floatPoint(self): """Summary Returns: TYPE: Description """ pt = self.pos() return pt.x(), pt.y() # end def def isForward(self): """Summary Returns: TYPE: Description """ return self.is_forward # end def def updatePositionAndAppearance(self, is_from_strand=True): """ Sets position by asking the VirtualHelixItem Sets appearance by choosing among pre-defined painterpaths (from normalstrandgraphicsitem) depending on drawing direction. Args: is_from_strand (bool, optional): Description """ self.setPos(*self.point()) n5 = self._xover_item._node5 if is_from_strand: from_strand, from_idx = (n5._strand, n5._idx) if n5 != self else (None, None) if self._strand.canInstallXoverAt(self._idx, from_strand, from_idx): self.configurePath() # We can only expose a 5' end. But on which side? is_left = True if self.is_forward else False self._updateLabel(is_left) else: self.hideItems() else: self.hideItems() # end def def remove(self): """Clean up this joint """ scene = self.scene() scene.removeItem(self._label) self._label = None scene.removeItem(self._path_thing) self._path_thing = None scene.removeItem(self._blank_thing) self._blank_thing = None scene.removeItem(self) # end def def _updateLabel(self, is_left): """Called by updatePositionAndAppearance during init. Updates drawing and position of the label. Args: is_left (TYPE): Description """ lbl = self._label if self._idx is not None: bw = _BASE_WIDTH num = self._partner_virtual_helix.idNum() tBR = _FM.tightBoundingRect(str(num)) half_label_h = tBR.height()/2.0 half_label_w = tBR.width()/2.0 # determine x and y positions label_x = bw/2.0 - half_label_w if self._is_on_top: label_y = -0.25*half_label_h - 0.5 - 0.5*bw else: label_y = 2*half_label_h + 0.5 + 0.5*bw # adjust x for left vs right label_x_offset = 0.25*bw if is_left else -0.25*bw label_x += label_x_offset # adjust x for numeral 1 if num == 1: label_x -= half_label_w/2.0 # create text item if lbl is None: lbl = QGraphicsSimpleTextItem(str(num), self) lbl.setPos(label_x, label_y) lbl.setBrush(_ENAB_BRUSH) lbl.setFont(_TO_HELIX_NUM_FONT) self._label = lbl lbl.setText(str(self._partner_virtual_helix.idNum())) lbl.show() # end if # end def def hideItems(self): """Summary Returns: TYPE: Description """ if self._label: self._label.hide() if self._blank_thing: self._path_thing.hide() if self._blank_thing: self._blank_thing.hide()
class PenArrowItem(QGraphicsItemGroup): def __init__(self, path, parent=None): super(PenArrowItem, self).__init__() self.saveable = True self.pi = QGraphicsPathItem() self.path = path self.animator = [parent] self.animateFlag = False # set arrowhead initial e0 = self.path.elementAt(0) e1 = self.path.elementAt(1) pti = QPointF(e1.x, e1.y) ptf = QPointF(e0.x, e0.y) delta = ptf - pti el = sqrt(delta.x()**2 + delta.y()**2) ndelta = delta / el northog = QPointF(-ndelta.y(), ndelta.x()) arBase = ptf - 16 * ndelta arTip = ptf + 8 * ndelta arLeft = arBase - 10 * northog - 4 * ndelta arRight = arBase + 10 * northog - 4 * ndelta self.ari = QPainterPath() self.ari.moveTo(ptf) self.ari.lineTo(arLeft) self.ari.lineTo(arBase) self.ari.lineTo(arRight) self.ari.lineTo(ptf) self.endi = QGraphicsPathItem() self.endi.setPath(self.ari) # set arrowhead final e2 = self.path.elementAt(self.path.elementCount() - 2) e3 = self.path.elementAt(self.path.elementCount() - 1) pti = QPointF(e2.x, e2.y) ptf = QPointF(e3.x, e3.y) delta = ptf - pti el = sqrt(delta.x()**2 + delta.y()**2) ndelta = delta / el northog = QPointF(-ndelta.y(), ndelta.x()) arBase = ptf - 16 * ndelta arTip = ptf + 8 * ndelta arLeft = arBase - 10 * northog - 4 * ndelta arRight = arBase + 10 * northog - 4 * ndelta self.arf = QPainterPath() self.arf.moveTo(ptf) self.arf.lineTo(arLeft) self.arf.lineTo(arBase) self.arf.lineTo(arRight) self.arf.lineTo(ptf) self.endf = QGraphicsPathItem() self.endf.setPath(self.arf) # put everything together self.pi.setPath(self.path) self.pi.setPen(QPen(Qt.red, 2)) self.endi.setPen(QPen(Qt.red, 2)) self.endi.setBrush(QBrush(Qt.red)) self.endf.setPen(QPen(Qt.red, 2)) self.endf.setBrush(QBrush(Qt.red)) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) self.addToGroup(self.pi) self.addToGroup(self.endi) self.addToGroup(self.endf) def itemChange(self, change, value): if change == QGraphicsItem.ItemPositionChange and self.scene(): command = CommandMoveItem(self, value) self.scene().undoStack.push(command) return QGraphicsItemGroup.itemChange(self, change, value) def pickle(self): pth = [] for k in range(self.path.elementCount()): # e should be either a moveTo or a lineTo e = self.path.elementAt(k) if e.isMoveTo(): pth.append(["m", e.x + self.x(), e.y + self.y()]) else: if e.isLineTo(): pth.append(["l", e.x + self.x(), e.y + self.y()]) else: log.error("Problem pickling penarrowitem path {}".format( self.path)) return ["PenArrow", pth] def paint(self, painter, option, widget): if not self.collidesWithItem(self.scene().underImage, mode=Qt.ContainsItemShape): # paint a bounding rectangle out-of-bounds warning painter.setPen(QPen(QColor(255, 165, 0), 8)) painter.setBrush(QBrush(QColor(255, 165, 0, 128))) painter.drawRoundedRect(option.rect, 10, 10) # paint the normal item with the default 'paint' method super(PenArrowItem, self).paint(painter, option, widget)
class ForcedXoverNode3(QGraphicsRectItem): """ This is a QGraphicsRectItem to allow actions and also a QGraphicsSimpleTextItem to allow a label to be drawn Attributes: is_forward (TYPE): Description """ def __init__(self, virtual_helix_item, xover_item, strand3p, idx): """Summary Args: virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): from vhi xover_item (TYPE): Description strand3p (Strand): reference to the 3' strand idx (int): the base index within the virtual helix """ super(ForcedXoverNode3, self).__init__(virtual_helix_item) self._vhi = virtual_helix_item self._xover_item = xover_item self._idx = idx self.is_forward = strand3p.strandSet().isForward() self._is_on_top = self.is_forward self._partner_virtual_helix = virtual_helix_item self._blank_thing = QGraphicsRectItem(_blankRect, self) self._blank_thing.setBrush(QBrush(Qt.white)) self._path_thing = QGraphicsPathItem(self) self.configurePath() self._label = None self.setPen(_NO_PEN) self.setBrush(_NO_BRUSH) self.setRect(_rect) self.setZValue(styles.ZENDPOINTITEM + 1) # end def def updateForFloatFromVHI(self, virtual_helix_item, is_forward, idx_x, idx_y): """ Args: virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): Description is_forward (TYPE): Description idx_x (TYPE): Description idx_y (TYPE): Description """ self._vhi = virtual_helix_item self.setParentItem(virtual_helix_item) self._idx = idx_x self._is_on_top = self.is_forward = True if is_forward else False self.updatePositionAndAppearance(is_from_strand=False) # end def def updateForFloatFromStrand(self, virtual_helix_item, strand3p, idx): """ Args: virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): Description strand3p (Strand): reference to the 3' strand idx (int): the base index within the virtual helix """ self._vhi = virtual_helix_item self._strand = strand3p self.setParentItem(virtual_helix_item) self._idx = idx self._is_on_top = self.is_forward = strand3p.strandSet().isForward() self.updatePositionAndAppearance() # end def def configurePath(self): """Summary Returns: TYPE: Description """ self._path_thing.setBrush(getBrushObj(_PENCIL_COLOR)) path = PPR3 if self.is_forward else PPL3 offset = -_BASE_WIDTH if self.is_forward else _BASE_WIDTH self._path_thing.setPath(path) self._path_thing.setPos(offset, 0) offset = -_BASE_WIDTH if self.is_forward else 0 self._blank_thing.setPos(offset, 0) self._blank_thing.show() self._path_thing.show() # end def def refreshXover(self): """Summary Returns: TYPE: Description """ self._xover_item.refreshXover() # end def def setPartnerVirtualHelix(self, virtual_helix_item): """Summary Args: virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): Description Returns: TYPE: Description """ self._partner_virtual_helix = virtual_helix_item # end def def idx(self): """Summary Returns: TYPE: Description """ return self._idx # end def def virtualHelixItem(self): """Summary Returns: TYPE: Description """ return self._vhi # end def def point(self): """Summary Returns: TYPE: Description """ return self._vhi.upperLeftCornerOfBaseType(self._idx, self.is_forward) # end def def floatPoint(self): """Summary Returns: TYPE: Description """ pt = self.pos() return pt.x(), pt.y() # end def def isForward(self): """Summary Returns: TYPE: Description """ return self.is_forward # end def def updatePositionAndAppearance(self, is_from_strand=True): """ Sets position by asking the VirtualHelixItem Sets appearance by choosing among pre-defined painterpaths (from normalstrandgraphicsitem) depending on drawing direction. Args: is_from_strand (bool, optional): Description """ self.setPos(*self.point()) n5 = self._xover_item._node5 if is_from_strand: from_strand, from_idx = (n5._strand, n5._idx) if n5 != self else (None, None) if self._strand.canInstallXoverAt(self._idx, from_strand, from_idx): self.configurePath() # We can only expose a 5' end. But on which side? is_left = True if self.is_forward else False self._updateLabel(is_left) else: self.hideItems() else: self.hideItems() # end def def remove(self): """Clean up this joint """ scene = self.scene() scene.removeItem(self._label) self._label = None scene.removeItem(self._path_thing) self._path_thing = None scene.removeItem(self._blank_thing) self._blank_thing = None scene.removeItem(self) # end def def _updateLabel(self, is_left): """Called by updatePositionAndAppearance during init. Updates drawing and position of the label. Args: is_left (TYPE): Description """ lbl = self._label if self._idx is not None: bw = _BASE_WIDTH num = self._partner_virtual_helix.idNum() tBR = _FM.tightBoundingRect(str(num)) half_label_h = tBR.height()/2.0 half_label_w = tBR.width()/2.0 # determine x and y positions label_x = bw/2.0 - half_label_w if self._is_on_top: label_y = -0.25*half_label_h - 0.5 - 0.5*bw else: label_y = 2*half_label_h + 0.5 + 0.5*bw # adjust x for left vs right label_x_offset = 0.25*bw if is_left else -0.25*bw label_x += label_x_offset # adjust x for numeral 1 if num == 1: label_x -= half_label_w/2.0 # create text item if lbl is None: lbl = QGraphicsSimpleTextItem(str(num), self) lbl.setPos(label_x, label_y) lbl.setBrush(_ENAB_BRUSH) lbl.setFont(_TO_HELIX_NUM_FONT) self._label = lbl lbl.setText(str(self._partner_virtual_helix.idNum())) lbl.show() # end if # end def def hideItems(self): """Summary Returns: TYPE: Description """ if self._label: self._label.hide() if self._blank_thing: self._path_thing.hide() if self._blank_thing: self._blank_thing.hide()
class PreXoverItem(QGraphicsRectItem): """A PreXoverItem exists between a single 'from' VirtualHelixItem index and zero or more 'to' VirtualHelixItem Indices Attributes: adapter (TYPE): Description idx (int): the base index within the virtual helix is_fwd (TYPE): Description prexoveritemgroup (TYPE): Description to_vh_id_num (TYPE): Description """ def __init__(self, from_virtual_helix_item, is_fwd, from_index, to_vh_id_num, prexoveritemgroup, color): """Summary Args: from_virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): Description is_fwd (TYPE): Description from_index (TYPE): Description to_vh_id_num (TYPE): Description prexoveritemgroup (TYPE): Description color (TYPE): Description """ super(QGraphicsRectItem, self).__init__(BASE_RECT, from_virtual_helix_item) self.adapter = PropertyWrapperObject(self) self._bond_item = QGraphicsPathItem(self) self._bond_item.hide() self._label = PreXoverLabel(is_fwd, color, self) self._phos_item = Triangle(FWDPHOS_PP, self) self.setPen(getNoPen()) self.resetItem(from_virtual_helix_item, is_fwd, from_index, to_vh_id_num, prexoveritemgroup, color) # end def def shutdown(self): """Summary Returns: TYPE: Description """ self.setBrush(getBrushObj(self._color, alpha=0)) self.to_vh_id_num = None self.adapter.resetAnimations() phos = self._phos_item phos.adapter.resetAnimations() phos.resetTransform() phos.setPos(0, 0) self.setAcceptHoverEvents(False) self.setFlag(KEYINPUT_ACTIVE_FLAG, False) self.hide() # end def def resetItem(self, from_virtual_helix_item, is_fwd, from_index, to_vh_id_num, prexoveritemgroup, color): """Summary Args: from_virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): Description is_fwd (TYPE): Description from_index (TYPE): Description to_vh_id_num (TYPE): Description prexoveritemgroup (TYPE): Description color (TYPE): Description Returns: TYPE: Description """ self.setParentItem(from_virtual_helix_item) self.resetTransform() self._id_num = from_virtual_helix_item.idNum() self.idx = from_index self.is_fwd = is_fwd self.to_vh_id_num = to_vh_id_num self._color = color self.prexoveritemgroup = prexoveritemgroup self._bond_item.hide() self._label_txt = lbt = None if to_vh_id_num is None else str(to_vh_id_num) self.setLabel(text=lbt) self._label.resetItem(is_fwd, color) phos = self._phos_item bonditem = self._bond_item if is_fwd: phos.setPath(FWDPHOS_PP) phos.setTransformOriginPoint(0, phos.boundingRect().center().y()) phos.setPos(0.5*BASE_WIDTH, BASE_WIDTH) phos.setPen(getNoPen()) phos.setBrush(getBrushObj(color)) bonditem.setPen(getPenObj(color, styles.PREXOVER_STROKE_WIDTH)) self.setPos(from_index*BASE_WIDTH, -BASE_WIDTH) else: phos.setPath(REVPHOS_PP) phos.setTransformOriginPoint(0, phos.boundingRect().center().y()) phos.setPos(0.5*BASE_WIDTH, 0) phos.setPen(getPenObj(color, 0.25)) phos.setBrush(getNoBrush()) bonditem.setPen(getPenObj(color, styles.PREXOVER_STROKE_WIDTH, penstyle=Qt.DotLine, capstyle=Qt.RoundCap)) self.setPos(from_index*BASE_WIDTH, 2*BASE_WIDTH) if to_vh_id_num is not None: inactive_alpha = PROX_ALPHA self.setBrush(getBrushObj(color, alpha=inactive_alpha)) else: self.setBrush(getBrushObj(color, alpha=0)) self.show() # end def def getInfo(self): """ Returns: Tuple: (from_id_num, is_fwd, from_index, to_vh_id_num) """ return (self._id_num, self.is_fwd, self.idx, self.to_vh_id_num) ### ACCESSORS ### def color(self): """Summary Returns: TYPE: Description """ return self._color def remove(self): """Summary Returns: TYPE: Description """ scene = self.scene() self.adapter.destroy() if scene: scene.removeItem(self._label) self._label = None self._phos_item.adapter.resetAnimations() self._phos_item.adapter = None scene.removeItem(self._phos_item) self._phos_item = None scene.removeItem(self._bond_item) self._bond_item = None self.adapter.resetAnimations() self.adapter = None scene.removeItem(self) # end defS ### EVENT HANDLERS ### def hoverEnterEvent(self, event): """Only if enableActive(True) is called hover and key events disabled by default Args: event (TYPE): Description """ self.setFocus(Qt.MouseFocusReason) self.prexoveritemgroup.updateModelActiveBaseInfo(self.getInfo()) self.setActiveHovered(True) status_string = "%d[%d]" % (self._id_num, self.idx) self.parentItem().window().statusBar().showMessage(status_string) # end def def hoverLeaveEvent(self, event): """Summary Args: event (TYPE): Description Returns: TYPE: Description """ self.prexoveritemgroup.updateModelActiveBaseInfo(None) self.setActiveHovered(False) self.clearFocus() self.parentItem().window().statusBar().showMessage("") # end def def keyPressEvent(self, event): """Summary Args: event (TYPE): Description Returns: TYPE: Description """ self.prexoveritemgroup.handlePreXoverKeyPress(event.key()) # end def ### PUBLIC SUPPORT METHODS ### def setLabel(self, text=None, outline=False): """Summary Args: text (None, optional): Description outline (bool, optional): Description Returns: TYPE: Description """ if text: self._label.setTextAndStyle(text=text, outline=outline) self._label.show() else: self._label.hide() # end def def animate(self, item, property_name, duration, start_value, end_value): """Summary Args: item (TYPE): Description property_name (TYPE): Description duration (TYPE): Description start_value (TYPE): Description end_value (TYPE): Description Returns: TYPE: Description """ b_name = property_name.encode('ascii') anim = item.adapter.getRef(property_name) if anim is None: anim = QPropertyAnimation(item.adapter, b_name) item.adapter.saveRef(property_name, anim) anim.setDuration(duration) anim.setStartValue(start_value) anim.setEndValue(end_value) anim.start() # end def def setActiveHovered(self, is_active): """Rotate phosphate Triangle if `self.to_vh_id_num` is not `None` Args: is_active (bool): whether or not the PreXoverItem is parented to the active VirtualHelixItem """ if is_active: self.setBrush(getBrushObj(self._color, alpha=128)) self.animate(self, 'brush_alpha', 1, 0, 128) # overwrite running anim # if self.to_vh_id_num is not None: self.animate(self._phos_item, 'rotation', 500, 0, -90) else: inactive_alpha = 0 if self.to_vh_id_num is None else PROX_ALPHA self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 1000, 128, inactive_alpha) self.animate(self._phos_item, 'rotation', 500, -90, 0) # end def def enableActive(self, is_active, to_vh_id_num=None): """Call on PreXoverItems created on the active VirtualHelixItem Args: is_active (TYPE): Description to_vh_id_num (None, optional): Description """ if is_active: self.to_vh_id_num = to_vh_id_num self.setAcceptHoverEvents(True) if to_vh_id_num is None: self.setLabel(text=None) self.setBrush(getBrushObj(self._color, alpha=0)) else: self.setLabel(text=str(to_vh_id_num)) inactive_alpha = PROX_ALPHA self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 1000, 128, inactive_alpha) self.setFlag(KEYINPUT_ACTIVE_FLAG, True) else: self.setBrush(getNoBrush()) # self.setLabel(text=None) self.setAcceptHoverEvents(False) self.setFlag(KEYINPUT_ACTIVE_FLAG, False) def activateNeighbor(self, active_prexoveritem, shortcut=None): """To be called with whatever the active_prexoveritem is for the parts `active_base` Args: active_prexoveritem (TYPE): Description shortcut (None, optional): Description """ p1 = self._phos_item.scenePos() p2 = active_prexoveritem._phos_item.scenePos() scale = 3 delta1 = -BASE_WIDTH*scale if self.is_fwd else BASE_WIDTH*scale delta2 = BASE_WIDTH*scale if active_prexoveritem.is_fwd else -BASE_WIDTH*scale c1 = self.mapFromScene(QPointF(p1.x(), p1.y() + delta1)) c2 = self.mapFromScene(QPointF(p2.x(), p2.y() - delta2)) pp = QPainterPath() pp.moveTo(self._phos_item.pos()) pp.cubicTo(c1, c2, self._bond_item.mapFromScene(p2)) self._bond_item.setPath(pp) self._bond_item.show() alpha = 32 idx, active_idx = self.idx, active_prexoveritem.idx if self.is_fwd != active_prexoveritem.is_fwd: if idx == active_idx: alpha = 255 elif idx == active_idx + 1: alpha = 255 elif idx == active_idx - 1: alpha = 255 inactive_alpha = PROX_ALPHA if self.to_vh_id_num is not None else 0 self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 500, inactive_alpha, alpha) self.animate(self._phos_item, 'rotation', 500, 0, -90) self.setLabel(text=shortcut, outline=True) # end def def deactivateNeighbor(self): """Summary Returns: TYPE: Description """ if self.isVisible(): inactive_alpha = PROX_ALPHA if self.to_vh_id_num is not None else 0 self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 1000, 128, inactive_alpha) self.animate(self._phos_item, 'rotation', 500, -90, 0) self._bond_item.hide() self.setLabel(text=self._label_txt)
class PreXoverItem(QGraphicsRectItem): """A PreXoverItem exists between a single 'from' VirtualHelixItem index and zero or more 'to' VirtualHelixItem Indices Attributes: adapter (TYPE): Description idx (int): the base index within the virtual helix is_fwd (TYPE): Description prexoveritemgroup (TYPE): Description to_vh_id_num (TYPE): Description """ def __init__(self, from_virtual_helix_item, is_fwd, from_index, to_vh_id_num, prexoveritemgroup, color): """Summary Args: from_virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): Description is_fwd (TYPE): Description from_index (TYPE): Description to_vh_id_num (TYPE): Description prexoveritemgroup (TYPE): Description color (TYPE): Description """ super(QGraphicsRectItem, self).__init__(BASE_RECT, from_virtual_helix_item) self.adapter = PropertyWrapperObject(self) self._bond_item = QGraphicsPathItem(self) self._bond_item.hide() self._label = PreXoverLabel(is_fwd, color, self) self._phos_item = Triangle(FWDPHOS_PP, self) self.setPen(getNoPen()) self.resetItem(from_virtual_helix_item, is_fwd, from_index, to_vh_id_num, prexoveritemgroup, color) # end def def shutdown(self): """Summary Returns: TYPE: Description """ self.setBrush(getBrushObj(self._color, alpha=0)) self.to_vh_id_num = None self.adapter.resetAnimations() phos = self._phos_item phos.adapter.resetAnimations() phos.resetTransform() phos.setPos(0, 0) self.setAcceptHoverEvents(False) self.setFlag(KEYINPUT_ACTIVE_FLAG, False) self.hide() # end def def resetItem(self, from_virtual_helix_item, is_fwd, from_index, to_vh_id_num, prexoveritemgroup, color): """Summary Args: from_virtual_helix_item (cadnano.gui.views.pathview.virtualhelixitem.VirtualHelixItem): Description is_fwd (TYPE): Description from_index (TYPE): Description to_vh_id_num (TYPE): Description prexoveritemgroup (TYPE): Description color (TYPE): Description Returns: TYPE: Description """ self.setParentItem(from_virtual_helix_item) self.resetTransform() self._id_num = from_virtual_helix_item.idNum() self.idx = from_index self.is_fwd = is_fwd self.to_vh_id_num = to_vh_id_num self._color = color self.prexoveritemgroup = prexoveritemgroup self._bond_item.hide() self._label_txt = lbt = None if to_vh_id_num is None else str( to_vh_id_num) self.setLabel(text=lbt) self._label.resetItem(is_fwd, color) phos = self._phos_item bonditem = self._bond_item if is_fwd: phos.setPath(FWDPHOS_PP) phos.setTransformOriginPoint(0, phos.boundingRect().center().y()) phos.setPos(0.5 * BASE_WIDTH, BASE_WIDTH) phos.setPen(getNoPen()) phos.setBrush(getBrushObj(color)) bonditem.setPen(getPenObj(color, styles.PREXOVER_STROKE_WIDTH)) self.setPos(from_index * BASE_WIDTH, -BASE_WIDTH) else: phos.setPath(REVPHOS_PP) phos.setTransformOriginPoint(0, phos.boundingRect().center().y()) phos.setPos(0.5 * BASE_WIDTH, 0) phos.setPen(getPenObj(color, 0.25)) phos.setBrush(getNoBrush()) bonditem.setPen( getPenObj(color, styles.PREXOVER_STROKE_WIDTH, penstyle=Qt.DotLine, capstyle=Qt.RoundCap)) self.setPos(from_index * BASE_WIDTH, 2 * BASE_WIDTH) if to_vh_id_num is not None: inactive_alpha = PROX_ALPHA self.setBrush(getBrushObj(color, alpha=inactive_alpha)) else: self.setBrush(getBrushObj(color, alpha=0)) self.show() # end def def getInfo(self): """ Returns: Tuple: (from_id_num, is_fwd, from_index, to_vh_id_num) """ return (self._id_num, self.is_fwd, self.idx, self.to_vh_id_num) ### ACCESSORS ### def color(self): """Summary Returns: TYPE: Description """ return self._color def remove(self): """Summary Returns: TYPE: Description """ scene = self.scene() self.adapter.destroy() if scene: scene.removeItem(self._label) self._label = None self._phos_item.adapter.resetAnimations() self._phos_item.adapter = None scene.removeItem(self._phos_item) self._phos_item = None scene.removeItem(self._bond_item) self._bond_item = None self.adapter.resetAnimations() self.adapter = None scene.removeItem(self) # end defS ### EVENT HANDLERS ### def hoverEnterEvent(self, event): """Only if enableActive(True) is called hover and key events disabled by default Args: event (TYPE): Description """ self.setFocus(Qt.MouseFocusReason) self.prexoveritemgroup.updateModelActiveBaseInfo(self.getInfo()) self.setActiveHovered(True) status_string = "%d[%d]" % (self._id_num, self.idx) self.parentItem().window().statusBar().showMessage(status_string) # end def def hoverLeaveEvent(self, event): """Summary Args: event (TYPE): Description Returns: TYPE: Description """ self.prexoveritemgroup.updateModelActiveBaseInfo(None) self.setActiveHovered(False) self.clearFocus() self.parentItem().window().statusBar().showMessage("") # end def def keyPressEvent(self, event): """Summary Args: event (TYPE): Description Returns: TYPE: Description """ self.prexoveritemgroup.handlePreXoverKeyPress(event.key()) # end def ### PUBLIC SUPPORT METHODS ### def setLabel(self, text=None, outline=False): """Summary Args: text (None, optional): Description outline (bool, optional): Description Returns: TYPE: Description """ if text: self._label.setTextAndStyle(text=text, outline=outline) self._label.show() else: self._label.hide() # end def def animate(self, item, property_name, duration, start_value, end_value): """Summary Args: item (TYPE): Description property_name (TYPE): Description duration (TYPE): Description start_value (TYPE): Description end_value (TYPE): Description Returns: TYPE: Description """ b_name = property_name.encode('ascii') anim = item.adapter.getRef(property_name) if anim is None: anim = QPropertyAnimation(item.adapter, b_name) item.adapter.saveRef(property_name, anim) anim.setDuration(duration) anim.setStartValue(start_value) anim.setEndValue(end_value) anim.start() # end def def setActiveHovered(self, is_active): """Rotate phosphate Triangle if `self.to_vh_id_num` is not `None` Args: is_active (bool): whether or not the PreXoverItem is parented to the active VirtualHelixItem """ if is_active: self.setBrush(getBrushObj(self._color, alpha=128)) self.animate(self, 'brush_alpha', 1, 0, 128) # overwrite running anim # if self.to_vh_id_num is not None: self.animate(self._phos_item, 'rotation', 500, 0, -90) else: inactive_alpha = 0 if self.to_vh_id_num is None else PROX_ALPHA self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 1000, 128, inactive_alpha) self.animate(self._phos_item, 'rotation', 500, -90, 0) # end def def enableActive(self, is_active, to_vh_id_num=None): """Call on PreXoverItems created on the active VirtualHelixItem Args: is_active (TYPE): Description to_vh_id_num (None, optional): Description """ if is_active: self.to_vh_id_num = to_vh_id_num self.setAcceptHoverEvents(True) if to_vh_id_num is None: self.setLabel(text=None) self.setBrush(getBrushObj(self._color, alpha=0)) else: self.setLabel(text=str(to_vh_id_num)) inactive_alpha = PROX_ALPHA self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 1000, 128, inactive_alpha) self.setFlag(KEYINPUT_ACTIVE_FLAG, True) else: self.setBrush(getNoBrush()) # self.setLabel(text=None) self.setAcceptHoverEvents(False) self.setFlag(KEYINPUT_ACTIVE_FLAG, False) def activateNeighbor(self, active_prexoveritem, shortcut=None): """To be called with whatever the active_prexoveritem is for the parts `active_base` Args: active_prexoveritem (TYPE): Description shortcut (None, optional): Description """ p1 = self._phos_item.scenePos() p2 = active_prexoveritem._phos_item.scenePos() scale = 3 delta1 = -BASE_WIDTH * scale if self.is_fwd else BASE_WIDTH * scale delta2 = BASE_WIDTH * scale if active_prexoveritem.is_fwd else -BASE_WIDTH * scale c1 = self.mapFromScene(QPointF(p1.x(), p1.y() + delta1)) c2 = self.mapFromScene(QPointF(p2.x(), p2.y() - delta2)) pp = QPainterPath() pp.moveTo(self._phos_item.pos()) pp.cubicTo(c1, c2, self._bond_item.mapFromScene(p2)) self._bond_item.setPath(pp) self._bond_item.show() alpha = 32 idx, active_idx = self.idx, active_prexoveritem.idx if self.is_fwd != active_prexoveritem.is_fwd: if idx == active_idx: alpha = 255 elif idx == active_idx + 1: alpha = 255 elif idx == active_idx - 1: alpha = 255 inactive_alpha = PROX_ALPHA if self.to_vh_id_num is not None else 0 self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 500, inactive_alpha, alpha) self.animate(self._phos_item, 'rotation', 500, 0, -90) self.setLabel(text=shortcut, outline=True) # end def def deactivateNeighbor(self): """Summary Returns: TYPE: Description """ if self.isVisible(): inactive_alpha = PROX_ALPHA if self.to_vh_id_num is not None else 0 self.setBrush(getBrushObj(self._color, alpha=inactive_alpha)) self.animate(self, 'brush_alpha', 1000, 128, inactive_alpha) self.animate(self._phos_item, 'rotation', 500, -90, 0) self._bond_item.hide() self.setLabel(text=self._label_txt)
class Edge(QGraphicsPathItem): """Muchia dintre 2 noduri Muchia dintre 2 noduri este un path de la primul nod al muchiei la cel de-al doilea nod. In principiu path-ul este o line dreapta, dar daca un alt nod se intersecteaza cu aceast path, ea se va cruba pentru claritatea grafului. Atribute -------- node1 : Node primul nod al muchiei node2 : Node cel de-al doilea nod al muchiei engine : GraphEngine enginu-ul aplicatiei cost : int, optional costul muchiei arrow_length : int, 15 reprezinta lungimea sagetii muchiei in cazul unui graf orientat direct_path : QPainterPath path-ul direct de la primul la al doilea nod Metode ------ create_path(start_point, end_point, directed) creaza path-ul muchiei dintre cele 2 noduri handle_value_changed(value) schimba culoarea muchiei """ def __init__(self, node1, node2, engine, cost=None): super().__init__() self.node1 = node1 self.node2 = node2 self.engine = engine self.cost = cost self.arrow_length = 15 self.setPen(self.node1.pen) # Crearea unui path invizibil si adaugarea lui in scene self.direct_path = QGraphicsPathItem() self.direct_path.setPen(QPen(QColor(0, 0, 0, 0))) self.engine.view.scene.addItem(self.direct_path) def create_path(self, start_point, end_point, directed): """Creeaza path-ul muchiei Path-ul muchiei este o curba Bezier. In cazul in care nici-un nod nu se intersecteaza cu path-ul direct dintre noduri, punctele de control ale curbei vor fi la centrul de greutate al dreptei date de cele 2 noduri, astfel creeandu-se o linie dreapta. In caz contrar, daca un nod se intersecteaza cu path-ul direct, pucntele de control ale curbei se vor situa pe dreapta perpendiculara pe path-ul direct, ce trece centrul de greutate al acestuia (dat de punctul de control initial) la o distanta egala dublul razei nodului. Aceste pucnte se pot situa in 2 pozitii, una la 'stanga' path-ului, iar cealalta la 'dreaptea' acestuia. Pozitia finala a punctului de control se determina 'trasand' 2 linii de la nodul care se intersecteaza la cele 2 posibile puncte de control. Verificand lungimea celor 2 linii se alege locatia punctului de control. panta dreptei : m = (y2 - y1) / (x2 - x1) ecuatia dreptei : y - y1 = m(x - x1) panta drepntei perpendiculare pe o dreapta : m' = -1 / m lungimea unei drepte : AB ^ 2 = (x2 - x1) ^ 2 + (y2 - y1) ^ 2 => primul pas pentru a afla pucntele de control in cazul unei intersectii este: de a calula panta dreptei perpendiculara pe path-ul direct => m' = -1 / (node2.y - node1.y) / (node2.x - node1.x) => m' = -1 * (node2.x - node1.x) / (node2.y - node1.y) => cel de-al doilea pas este calcularea ecuatiei dreptei de panta m' ce trece prin pucntul de control (not G) => y - G.y = m'(x - G.x) => y = m'(x - G.x) + G.y => cel de-al treilea pas este inlocuirea lui y in lungimea dreptei ( lungimea dreptei dorita este dublul razei nodului) pentru a afla cele 2 coordonate x posibile (la 'stanga' si la 'dreapta' path-ului direct) => (x2 - G.x) ^ 2 + (m'(x2 - G.x) + G.y - G.y) ^ 2 = (2raza) ^ 2 => x2 ^ 2 - 2 x2 G.x + G.x ^ 2 + (m' x2) ^ 2 - 2 (m' ^ 2) x2 G.x + (m' G.x) ^ 2 - (2raza) ^ 2 = 0 => (x2 ^ 2)(1 + m' ^ 2) + x2(2 G.x (1 + m' ^ 2)) + (G.x ^ 2)(1 + m' ^ 2) - (2raza) ^ 2 = 0 => cele 2 coordonate pe Ox ale punctului de control, prentu a afla cele 2 coordonate pe Oy se inlocuiesc valorie obtinute in ecuatia dreptei. Parametrii ---------- start_point : QPointF punctul de start al path-ului end_point : QPointF punctul de final al path-ului directed : bool orientarea grafului Returneaza ---------- path : QPainterPath path-ul final al muchiei """ # Centrul de greutate al dreptei formata de cele 2 noduri control_point = QPointF((start_point.x() + end_point.x()) / 2, (start_point.y() + end_point.y()) / 2) path = QPainterPath(start_point) node_radius = self.engine.node_radius point1 = point2 = None # Creearea path-ului direct _path = QPainterPath(start_point) _path.lineTo(end_point) self.direct_path.setPath(_path) # Verificarea pentru intersectii cu path-ul direct intersecting_items = self.engine.view.scene.collidingItems( self.direct_path) intersecting_items.remove(self.node1) intersecting_items.remove(self.node2) # Calcularea coordonatelor pe Ox a punctelor de control in cazul unei intersectii try: m = -1 * (self.node2.x() - self.node1.x()) / (self.node2.y() - self.node1.y()) agent = 1 + (m**2) factors = [ agent, -2 * control_point.x() * agent, (control_point.x()**2) * agent - (node_radius * 2)**2 ] roots = np.roots(factors) # In cazul in care nodurile au acceleasi coordonate pe Ox sau Oy panta # dreptei nu exista. Atunci se va trata cazul de ZeroDivisionError except ZeroDivisionError: point1 = control_point + QPointF(0, node_radius * 2) point2 = control_point - QPointF(0, node_radius * 2) for item in intersecting_items: if isinstance(item, Node): # Daca exista o intersectie si exista si panta dreptei atunci se calculeaza # si coordonatele pe Oy ale posibilelor puncte de control if (point1 and point2) is None: point1 = QPointF( roots[0], m * (roots[0] - control_point.x()) + control_point.y()) point2 = QPointF( roots[1], m * (roots[1] - control_point.x()) + control_point.y()) # Cele 2 linii de la nod la posibilele puncte de control line1 = QLineF(item.pos(), point1) line2 = QLineF(item.pos(), point2) # Daca lungimea primei linii este mai mica decat lungimea celei de-a doua linie # inseamna ca nodul este mai aproape de prima linie deci path-ul va trebui sa se # curbeze in partea opusa => se alege cel de-al doilea punct control_point = point2 if line1.length() <= line2.length( ) else point1 break # Creearea curbei Bezier path.cubicTo(control_point, control_point, end_point) # Daca graful este orientat se adauga la capatul muchiei o sageata pentru # a reprezenta orientarea acestuia if directed: pos = path.currentPosition() dx, dy, angle = self.engine.get_angle(control_point, end_point) path.lineTo( QPointF(pos.x() + self.arrow_length * math.cos(angle + 60), pos.y() + self.arrow_length * math.sin(angle + 60))) path.moveTo(end_point) path.lineTo( QPointF(pos.x() + self.arrow_length * math.cos(angle - 60), pos.y() + self.arrow_length * math.sin(angle - 60))) # In cazul in care muchia are un cost acesta va fi afisat la mijlocul muchiei font_metrics = QFontMetrics(TEXT_FONT) font_offset = QPointF(font_metrics.height(), font_metrics.horizontalAdvance(self.cost)) path.addText(control_point - font_offset / 2, TEXT_FONT, self.cost) return path def handle_value_changed(self, value): """Schimba culoarea muchiei Aceasta metoda este folosita pentru shimbarea culorii in timpul unei animatii Parametrii ---------- value : QColor noua culoare a muchiei """ self.setPen(QPen(value, 1.5, Qt.SolidLine))
class CharItem(QGraphicsRectItem): """ This item represents character item The purpose of the class is to draw a character, create a matrix of rectangles and resolve in which rectangles the character passes The class allow the following operations -# Drawing a character using the mouse events: -# Start by the mouse press event -# Continues by the mouse move event -# The character is stored in QGraphicsPathItem -# Transform the character to occupy the whole item's space -# Set operation : resolving the Occupied matrix which tell on which rectangle the character passes -# Reset operation : reverse the character transform so it is possible to continue drawing the character -# Save operation : To a QDataStream -# Load operation : From a QDataStream The graphical view of the class is composed from: -# This class which inherits from QGraphicsRectItem and holds : -# A QGraphicsPathItem : representing the character -# A QGraphicsPathItem : representing the occupied rectangles -# A QGraphicsPathItem : representing the unoccupied rectangles """ def __init__(self, rect: QRectF, pos: QPointF, viewIndex: int = -1): """ CharItem constructor Args: rect (QRectF) : The rectangle that the character should fill pos (QPointF) : The position of the item within the parent viewIndex (int) : The index of the item in case it is presented in multi character presentation """ super(CharItem, self).__init__(rect) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPresentationPrms() self.occupied = [[False for idx in range(self.netCols)] for idx in range(self.netRows)] self.charPath = None self.wasSetted = False self.occupiedPathItem = None self.unoccupiedPathItem = None self.dirty = False self.viewIndex = viewIndex self.filename = "" self.boundaries = rect self.dx = 1 self.dy = 1 self.posInParent = pos self.setPos(self.posInParent) def setPresentationPrms(self): """ Setting the presentation prms The reason the there is a duplicate set of presentation parameters is that it allows changing the presentation parameters for one character (like in the select option """ self.netColor = netColor self.netThickness = netThickness self.occupyColor = occupyColor self.unOccupyColor = unOccupyColor self.shapeColor = shapeColor self.shapeLineThickness = shapeLineThickness self.selectedOccupiedColor = selectedOccupiedColor self.selectedShapeColor = selectedShapeColor self.netRows = netRows self.netCols = netCols def setNetBoxDimensions(self, rect: QRectF): """ Set net box dimensions The net box is the rectangle that compose the network drawn to show the occupy matrix """ self.netRectHeight = rect.height() / self.netRows self.netRectWidth = rect.width() / self.netCols self.left = rect.left() self.top = rect.top() def netRect(self, row_idx: int, col_idx: int) -> QRectF: """ Set net rect The net box is the rectangle that compose the network drawn to show the occupy matrix Args: row_idx (int) : The row of the network rectangle col_idx (int) : The col of the network rectangle Returns: QRectF : The rectangle """ return QRectF(self.left + col_idx * self.netRectWidth, self.top + row_idx * self.netRectHeight, self.netRectWidth, self.netRectHeight) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse move event : continue draw a line This event is activated when the mouse is pressed and moves The methods draws the line in 2 conditions: -# The item is not part of multi character presentation -# A character path was initiated (using the mouse press event) Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: if self.charPath is not None: point = event.scenePos() path = self.charPath.path() path.lineTo(point) self.charPath.setPath(path) self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse Press Event : Start a new line / Select the character If the character is part of multi character presentation activate the character selection If the character is in single character presentation Start a new line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: self.startLine(event) else: self.setSelected() def startLine(self, event: QGraphicsSceneMouseEvent): """ Start drawing a line When the mouse button is pressed and we are in single character dialog this method is activated to start drowning a line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ # There are 2 modes for the presentation: # Original mode where the character is it's original size # After setting mode when the set was done and the character fullfill all # the item's space # Drawing can be done only in original mode if self.wasSetted: QMessageBox.critical( None, "Char identifier window", "The shape was already setted use revert setting") return # If this is the first start of a line - generate the QPainterPath and QGraphicsPathItem if self.charPath is None: self.initCharPath() # Move to the mouse position point = event.scenePos() path = self.charPath.path() path.moveTo(point) self.charPath.setPath(path) self.dirty = True def initCharPath(self): """ Init the item that holds the character There is one path item that holds the character This method is activated by start line if the char item was not created to create the new and only one """ self.dirty = True self.charPath = QGraphicsPathItem(self) self.charPath.setPen( QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.charPath.setZValue(1) self.charPath.originalPos = self.charPath.pos() self.charPath.setPath(QPainterPath()) def setSelected(self): """ Set the item a selected item This method is activated when the mouse button is presses and the item is part of multi character presentation """ # Set the colors of the item self.occupiedPathItem.setBrush( QBrush(QColor(self.selectedOccupiedColor))) self.charPath.setPen( QPen(QColor(self.selectedShapeColor), self.shapeLineThickness)) self.update() # Report to the parent item about the selection self.parentItem().setSelected(self.viewIndex) def resetSelected(self): """ Set the colors of the item to not selected """ self.occupiedPathItem.setBrush(QBrush(QColor(self.occupyColor))) self.charPath.setPen( QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.update() def set(self): """ Calculate the occupied matrix and present the results This method does the following: -# Fill the occupied matrix -# Generate the occupied and unoccupied pathes items -# Transform the char path to fit to the item's boundaries """ # If there is no shape drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is in setted mode - return if self.wasSetted: QMessageBox.critical( None, "Char identifier window", "The shape was already setted use revert setting") return # fill the occupied matrix with the data before the scaling self.fillOccupied() self.setNetBoxDimensions(self.boundingRect()) self.createNetPaths() # update the transform - change the dimensions and location # only on the first time self.transformCharPath() self.wasSetted = True # update the presentation self.update() def revertTransform(self): """ Change from Setted mode to drawing mode The drawing mode is the mode where the character can be drawn -# Restore the original size of the character (Reset the transform of the char item) -# Restor the char path item's position to the original one (saved when created) -# Empty the occupiedPath and the unoccupiedPath """ # If there is no character drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is already in drawing mode - return if not self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was not setted use set button") return # The char path item transform = self.charPath.transform() transform.reset() # The self.dx and self.dy are the scale parameters created when the item # begins and they are the scale parameters that transform it to the boundaries # given by the parent item transform.scale(self.dx, self.dy) self.charPath.setTransform(transform) self.charPath.setPos(self.charPath.originalPos) # Empty the network pathes self.occupiedPathItem.setPath(QPainterPath()) self.unoccupiedPathItem.setPath(QPainterPath()) self.wasSetted = False def transformCharPath(self): """ Transform char path when the item is setted This method does the following -# scale the char path to the size of the item -# calculate the new position of the char path so that it will be placed at the top left corner of the item """ dx = self.boundingRect().width() / self.charPath.boundingRect().width() dy = self.boundingRect().height() / self.charPath.boundingRect( ).height() transform = self.charPath.transform() transform.reset() transform.scale(dx, dy) self.charPath.setTransform(transform) # Move the shape to the origin moveX = -(self.charPath.boundingRect().left() - self.boundingRect().left()) * dx moveY = -(self.charPath.boundingRect().top() - self.boundingRect().top()) * dy self.charPath.setX(self.charPath.x() + moveX) self.charPath.setY(self.charPath.y() + moveY) def fillOccupied(self): """ Fill the occupied matrix The algorithm of filling the occupied matrix is -# Scanning the char path -# For each point decide on where row and column of the net -# Set the occupies matrix for this column and row to True """ for idx in range(100): point = self.charPath.path().pointAtPercent(idx / 100.) row_idx, col_idx = self.calcRowCol(point) self.occupied[row_idx][col_idx] = True def calcRowCol(self, point: QPointF): """ Calculate the network row and column that a point is int calc the row and column indexes of a point The following is the algorithm: 1. Find the distance between the point and the left (or top) 2. Divide the distance with the width of path to find the relative position 3. Multipile this relative position with the number of rows/cols 4. Convert the result to int to find the indexes 5. If the index is the number of row/col reduce the index (This is for the case the the point is on the boundary and in this case the relative position is 1 which will cause the indexes to be the number of rows/cols - out of the matrix indexes) Args: point (QPointF) : The point to resolve Returns: int : The network row that the point is in int : The network column that the point is in """ partialX = (point.x() - self.charPath.boundingRect().left() ) / self.charPath.boundingRect().width() partialY = (point.y() - self.charPath.boundingRect().top() ) / self.charPath.boundingRect().height() col_idx = int(partialX * self.netCols) row_idx = int(partialY * self.netRows) if row_idx == self.netRows: row_idx -= 1 if col_idx == self.netCols: col_idx -= 1 return row_idx, col_idx def createNetPaths(self): """ Create the network pathes This method creates 2 network pathes items one for holding the occupied rectangles and one to hold the unoccupied rectangles """ # Generate 2 QPainterPath occupiedPath = QPainterPath() unoccupiedPath = QPainterPath() # For each entry in occupied matrix : # Add a rectangle to the appropriate path according the entry value for row_idx in range(self.netRows): for col_idx in range(self.netCols): if self.occupied[row_idx][col_idx]: occupiedPath.addRect(self.netRect(row_idx, col_idx)) else: unoccupiedPath.addRect(self.netRect(row_idx, col_idx)) # Create the QGraphicsPathItems that will hold the path self.createNetPath(self.occupyColor, occupiedPath, True) self.createNetPath(self.unOccupyColor, unoccupiedPath, False) def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen( QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0) def save(self, stream: QDataStream, filename: str): """ Save the item to QDataStream Args: stream (QDataStream) : The data stream to write the item to filename (str) : The filename (for documenting purposes) """ # The item position stream << self.pos() # The dimensions stream << self.rect() # The presentation parameters stream.writeQString(self.netColor) stream.writeQString(self.occupyColor) stream.writeQString(self.unOccupyColor) stream.writeQString(self.shapeColor) stream.writeInt16(self.shapeLineThickness) stream.writeInt16(self.netRows) stream.writeInt16(self.netRows) # The items paths stream << self.charPath.path() self.dirty = False self.filename = filename def load(self, stream, filename): """ Loads the item from QDataStream Args: stream (QDataStream) : The data stream to read the item from filename (str) : The filename (for documenting purposes) """ # read the pos pos = QPointF() stream >> pos self.setPos(pos) # read the dimensions rect = QRectF() stream >> rect self.setRect(rect) # The presentation parameters self.netColor = stream.readQString() self.occupyColor = stream.readQString() self.unOccupyColor = stream.readQString() self.shapeColor = stream.readQString() self.shapeLineThickness = stream.readInt16() self.netRows = stream.readInt16() self.netRows = stream.readInt16() # read the paths self.initCharPath() path = self.charPath.path() stream >> path self.charPath.setPath(path) # Fit the item to the boundaries and position given by the item's parent self.fitToBoundaries() # The presentation of the item is in setted mode so we activate the set method self.wasSetted = False self.set() self.dirty = False self.filename = filename def fitToBoundaries(self): """ Fit the item to the boundaries and position given by it's parent This method was made to support the change of the character boundaries and that the char can be presented in different boundaries and position """ self.setPos(self.posInParent) self.dx = self.boundaries.width() / self.rect().width() self.dy = self.boundaries.height() / self.rect().height() transform = self.transform() transform.scale(self.dx, self.dy) self.setTransform(transform)
class NodeItem(QGraphicsObject): """ An widget node item in the canvas. """ #: Signal emitted when the scene position of the node has changed. positionChanged = Signal() #: Signal emitted when the geometry of the channel anchors changes. anchorGeometryChanged = Signal() #: Signal emitted when the item has been activated (by a mouse double #: click or a keyboard) activated = Signal() #: The item is under the mouse. hovered = Signal() #: Span of the anchor in degrees ANCHOR_SPAN_ANGLE = 90 #: Z value of the item Z_VALUE = 100 def __init__(self, widget_description=None, parent=None, **kwargs): self.__boundingRect = None QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QGraphicsItem.ItemHasNoContents, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) # central body shape item self.shapeItem = None # in/output anchor items self.inputAnchorItem = None self.outputAnchorItem = None # title text item self.captionTextItem = None # error, warning, info items self.errorItem = None self.warningItem = None self.infoItem = None # background when selected self.backgroundItem = None self.__title = "" self.__processingState = 0 self.__progress = -1 self.__statusMessage = "" self.__error = None self.__warning = None self.__info = None self.__anchorLayout = None self.__animationEnabled = False self.setZValue(self.Z_VALUE) self.setupGraphics() self.setWidgetDescription(widget_description) @classmethod def from_node(cls, node): """ Create an :class:`NodeItem` instance and initialize it from a :class:`SchemeNode` instance. """ self = cls() self.setWidgetDescription(node.description) # self.setCategoryDescription(node.category) return self @classmethod def from_node_meta(cls, meta_description): """ Create an `NodeItem` instance from a node meta description. """ self = cls() self.setWidgetDescription(meta_description) return self def setupGraphics(self): """ Set up the graphics. """ shape_rect = QRectF(-24, -24, 48, 48) self.shapeItem = NodeBodyItem(self) self.shapeItem.setShapeRect(shape_rect) self.shapeItem.setAnimationEnabled(self.__animationEnabled) # Rect for widget's 'ears'. anchor_rect = QRectF(-31, -31, 62, 62) self.inputAnchorItem = SinkAnchorItem(self) input_path = QPainterPath() start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 input_path.arcMoveTo(anchor_rect, start_angle) input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) self.inputAnchorItem.setAnchorPath(input_path) self.outputAnchorItem = SourceAnchorItem(self) output_path = QPainterPath() start_angle = self.ANCHOR_SPAN_ANGLE / 2 output_path.arcMoveTo(anchor_rect, start_angle) output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE) self.outputAnchorItem.setAnchorPath(output_path) self.inputAnchorItem.hide() self.outputAnchorItem.hide() # Title caption item self.captionTextItem = NameTextItem(self) self.captionTextItem.setPlainText("") self.captionTextItem.setPos(0, 33) def iconItem(standard_pixmap): item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), iconSize=QSize(16, 16)) item.hide() return item self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) ################################ # PyQt 5.10 crashes because of this call (2) #self.backgroundItem = QGraphicsPathItem(self) self.backgroundItem = QGraphicsPathItem(None) ################################ backgroundrect = QPainterPath() backgroundrect.addRoundedRect(anchor_rect.adjusted(-4, -2, 4, 2), 5, 5, mode=Qt.AbsoluteSize) self.backgroundItem.setPen(QPen(Qt.NoPen)) self.backgroundItem.setBrush(QPalette().brush(QPalette.Highlight)) self.backgroundItem.setOpacity(0.5) self.backgroundItem.setPath(backgroundrect) self.backgroundItem.setZValue(-10) self.backgroundItem.setVisible(self.isSelected()) self.prepareGeometryChange() self.__boundingRect = None # TODO: Remove the set[Widget|Category]Description. The user should # handle setting of icons, title, ... def setWidgetDescription(self, desc): """ Set widget description. """ self.widget_description = desc if desc is None: return icon = icon_loader.from_description(desc).get(desc.icon) if icon: self.setIcon(icon) if not self.title(): self.setTitle(desc.name) if desc.inputs: self.inputAnchorItem.show() if desc.outputs: self.outputAnchorItem.show() tooltip = NodeItem_toolTipHelper(self) self.setToolTip(tooltip) def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color) def setIcon(self, icon): """ Set the node item's icon (:class:`QIcon`). """ if isinstance(icon, QIcon): self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, iconSize=QSize(36, 36)) self.icon_item.setPos(-18, -18) else: raise TypeError def setColor(self, color, selectedColor=None): """ Set the widget color. """ if selectedColor is None: selectedColor = saturated(color, 150) palette = create_palette(color, selectedColor) self.shapeItem.setPalette(palette) def setPalette(self, palette): # TODO: The palette should override the `setColor` raise NotImplementedError def setTitle(self, title): """ Set the node title. The title text is displayed at the bottom of the node. """ self.__title = title self.__updateTitleText() def title(self): """ Return the node title. """ return self.__title title_ = Property(six.text_type, fget=title, fset=setTitle, doc="Node title text.") def setFont(self, font): """ Set the title text font (:class:`QFont`). """ if font != self.font(): self.prepareGeometryChange() self.captionTextItem.setFont(font) self.__updateTitleText() def font(self): """ Return the title text font. """ return self.captionTextItem.font() def setAnimationEnabled(self, enabled): """ Set the node animation enabled state. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled self.shapeItem.setAnimationEnabled(enabled) def animationEnabled(self): """ Are node animations enabled. """ return self.__animationEnabled def setProcessingState(self, state): """ Set the node processing state i.e. the node is processing (is busy) or is idle. """ if self.__processingState != state: self.__processingState = state self.shapeItem.setProcessingState(state) if not state: # Clear the progress meter. self.setProgress(-1) if self.__animationEnabled: self.shapeItem.ping() def processingState(self): """ The node processing state. """ return self.__processingState processingState_ = Property(int, fget=processingState, fset=setProcessingState) def setProgress(self, progress): """ Set the node work progress state (number between 0 and 100). """ if progress is None or progress < 0 or not self.__processingState: progress = -1 progress = max(min(progress, 100), -1) if self.__progress != progress: self.__progress = progress self.shapeItem.setProgress(progress) self.__updateTitleText() def progress(self): """ Return the node work progress state. """ return self.__progress progress_ = Property(float, fget=progress, fset=setProgress, doc="Node progress state.") def setStatusMessage(self, message): """ Set the node status message text. This text is displayed below the node's title. """ if self.__statusMessage != message: self.__statusMessage = six.text_type(message) self.__updateTitleText() def statusMessage(self): return self.__statusMessage def setStateMessage(self, message): """ Set a state message to display over the item. Parameters ---------- message : UserMessage Message to display. `message.severity` is used to determine the icon and `message.contents` is used as a tool tip. """ # TODO: Group messages by message_id not by severity # and deprecate set[Error|Warning|Error]Message if message.severity == UserMessage.Info: self.setInfoMessage(message.contents) elif message.severity == UserMessage.Warning: self.setWarningMessage(message.contents) elif message.severity == UserMessage.Error: self.setErrorMessage(message.contents) def setErrorMessage(self, message): if self.__error != message: self.__error = message self.__updateMessages() def setWarningMessage(self, message): if self.__warning != message: self.__warning = message self.__updateMessages() def setInfoMessage(self, message): if self.__info != message: self.__info = message self.__updateMessages() def newInputAnchor(self): """ Create and return a new input :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") anchor = AnchorPoint() self.inputAnchorItem.addAnchor(anchor, position=1.0) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) return anchor def removeInputAnchor(self, anchor): """ Remove input anchor. """ self.inputAnchorItem.removeAnchor(anchor) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) def newOutputAnchor(self): """ Create and return a new output :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") anchor = AnchorPoint(self) self.outputAnchorItem.addAnchor(anchor, position=1.0) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) return anchor def removeOutputAnchor(self, anchor): """ Remove output anchor. """ self.outputAnchorItem.removeAnchor(anchor) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) def inputAnchors(self): """ Return a list of all input anchor points. """ return self.inputAnchorItem.anchorPoints() def outputAnchors(self): """ Return a list of all output anchor points. """ return self.outputAnchorItem.anchorPoints() def setAnchorRotation(self, angle): """ Set the anchor rotation. """ self.inputAnchorItem.setRotation(angle) self.outputAnchorItem.setRotation(angle) self.anchorGeometryChanged.emit() def anchorRotation(self): """ Return the anchor rotation. """ return self.inputAnchorItem.rotation() def boundingRect(self): # TODO: Important because of this any time the child # items change geometry the self.prepareGeometryChange() # needs to be called. if self.__boundingRect is None: self.__boundingRect = self.childrenBoundingRect() return self.__boundingRect def shape(self): # Shape for mouse hit detection. # TODO: Should this return the union of all child items? return self.shapeItem.shape() def __updateTitleText(self): """ Update the title text item. """ text = ['<div align="center">%s' % escape(self.title())] status_text = [] progress_included = False if self.__statusMessage: msg = escape(self.__statusMessage) format_fields = dict(parse_format_fields(msg)) if "progress" in format_fields and len(format_fields) == 1: # Insert progress into the status text format string. spec, _ = format_fields["progress"] if spec != None: progress_included = True progress_str = "{0:.0f}%".format(self.progress()) status_text.append(msg.format(progress=progress_str)) else: status_text.append(msg) if self.progress() >= 0 and not progress_included: status_text.append("%i%%" % int(self.progress())) if status_text: text += ["<br/>", '<span style="font-style: italic">', "<br/>".join(status_text), "</span>"] text += ["</div>"] text = "".join(text) # The NodeItems boundingRect could change. self.prepareGeometryChange() self.__boundingRect = None self.captionTextItem.setHtml(text) self.captionTextItem.document().adjustSize() width = self.captionTextItem.textWidth() self.captionTextItem.setPos(-width / 2.0, 33) def __updateMessages(self): """ Update message items (position, visibility and tool tips). """ items = [self.errorItem, self.warningItem, self.infoItem] messages = [self.__error, self.__warning, self.__info] for message, item in zip(messages, items): item.setVisible(bool(message)) item.setToolTip(message or "") shown = [item for item in items if item.isVisible()] count = len(shown) if count: spacing = 3 rects = [item.boundingRect() for item in shown] width = sum(rect.width() for rect in rects) width += spacing * max(0, count - 1) height = max(rect.height() for rect in rects) origin = self.shapeItem.boundingRect().top() - spacing - height origin = QPointF(-width / 2, origin) for item, rect in zip(shown, rects): item.setPos(origin) origin = origin + QPointF(rect.width() + spacing, 0) def mousePressEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.mousePressEvent(self, event) else: event.ignore() def mouseDoubleClickEvent(self, event): if self.shapeItem.path().contains(event.pos()): QGraphicsObject.mouseDoubleClickEvent(self, event) QTimer.singleShot(0, self.activated.emit) else: event.ignore() def contextMenuEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.contextMenuEvent(self, event) else: event.ignore() def focusInEvent(self, event): self.shapeItem.setHasFocus(True) return QGraphicsObject.focusInEvent(self, event) def focusOutEvent(self, event): self.shapeItem.setHasFocus(False) return QGraphicsObject.focusOutEvent(self, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: selected = bool(qtcompat.qunwrap(value)) self.shapeItem.setSelected(selected) self.captionTextItem.setSelectionState(selected) self.backgroundItem.setVisible(selected) elif change == QGraphicsItem.ItemPositionHasChanged: self.positionChanged.emit() return QGraphicsObject.itemChange(self, change, value)