def __init__(self, model_virtual_helix, part_item): """ Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. part_item (cadnano.gui.views.sliceview.nucleicacidpartitem.NucleicAcidPartItem): the part item """ AbstractVirtualHelixItem.__init__(self, model_virtual_helix, part_item) QGraphicsEllipseItem.__init__(self, parent=part_item) self._controller = VirtualHelixItemController(self, self._model_part, False, True) self.hide() model_part = self._model_part x, y = model_part.locationQt(self._id_num, part_item.scaleFactor()) # set position to offset for radius # self.setTransformOriginPoint(_RADIUS, _RADIUS) self.setCenterPos(x, y) self.wedge_gizmos = {} self._added_wedge_gizmos = set() # self._prexo_gizmos = [] self.setAcceptHoverEvents(True) self.setZValue(_ZVALUE) # handle the label specific stuff self._label = self.createLabel() self.setNumber() self.old_pen = None self.is_active = False self.updateAppearance() self.show() self._right_mouse_move = False
def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = empty_helix_item self.hide() # drawing related self.isHovered = False self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(self._ZVALUE) self.lastMousePressAddedBases = False self.setBrush(self._OUT_OF_SLICE_BRUSH) self.setPen(self._OUT_OF_SLICE_PEN) self.setRect(self._RECT) # handle the label specific stuff self._label = self.createLabel() self.setNumber() self.createArrow() self._controller = VirtualHelixItemController(self, model_virtual_helix) self.show()
def __init__(self, part_item, model_virtual_helix, viewroot): super(VirtualHelixItem, self).__init__(part_item.proxy()) self._part_item = part_item self._model_virtual_helix = model_virtual_helix self._viewroot = viewroot self._getActiveTool = part_item._getActiveTool self._controller = VirtualHelixItemController(self, model_virtual_helix) self._handle = VirtualHelixHandleItem(model_virtual_helix, part_item, viewroot) self._last_strand_set = None self._last_idx = None self._scaffoldBackground = None self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption) self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.setBrush(QBrush(Qt.NoBrush)) view = viewroot.scene().views()[0] view.levelOfDetailChangedSignal.connect(self.levelOfDetailChangedSlot) shouldShowDetails = view.shouldShowDetails() pen = QPen(styles.MINOR_GRID_STROKE, styles.MINOR_GRID_STROKE_WIDTH) pen.setCosmetic(shouldShowDetails) self.setPen(pen) self.refreshPath() self.setAcceptHoverEvents(True) # for pathtools self.setZValue(styles.ZPATHHELIX)
def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = empty_helix_item self.hide() # drawing related self.isHovered = False self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(self._ZVALUE) self.lastMousePressAddedBases = False self.setBrush(self._OUT_OF_SLICE_BRUSH) self.setPen(self._OUT_OF_SLICE_PEN) self.setRect(self._RECT) # handle the label specific stuff self._label = self.createLabel() self.setNumber() self._pen1, self._pen2 = (QPen(), QPen()) self.createArrows() self._controller = VirtualHelixItemController(self, model_virtual_helix) self.show()
def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = empty_helix_item self._controller = VirtualHelixItemController(self, model_virtual_helix) self.hide() # drawing related self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(self._ZVALUE) self.lastMousePressAddedBases = False # handle the label specific stuff self._label = self.createLabel() self.setNumber() self._pen1, self._pen2 = (QPen(), QPen()) self.createArrows() self.updateProperty() self._rect = QRectF() self._hover_rect = QRectF() self._outer_line = RotateLine(self._rect, self) # self._inner_line = RotateLine(self._rect, self) self._hover_region = RotateHoverRegion(self._hover_rect, self) self.updateRects() self.show()
def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = ehi = empty_helix_item self._controller = VirtualHelixItemController(self, model_virtual_helix) self.hide() self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(_ZVALUE) self.lastMousePressAddedBases = False # handle the label specific stuff self._label = self.createLabel() self.setNumber() self._pen1, self._pen2 = (QPen(), QPen()) self.createArrows() self.updateAppearance() self._prexoveritemgroup = _pxig = PreXoverItemGroup(_RECT, self) _pxig.setTransformOriginPoint(_RECT.center()) # self._rect = QRectF(_RECT) # self._hover_rect = QRectF(_RECT) # self._outer_line = RotaryDialLine(self._rect, self) # self._hover_region = RotaryDialHoverRegion(self._hover_rect, self) self.show() # self._virtual_helix.setProperty('ehiX', ehi.mapToScene(0,0).x()) # self._virtual_helix.setProperty('ehiY', ehi.mapToScene(0,0).y()) self.getNeighbors()
def __init__(self, model_virtual_helix, part_item, viewroot): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. part_item (TYPE): Description viewroot (TYPE): Description """ AbstractVirtualHelixItem.__init__(self, model_virtual_helix, part_item) QGraphicsPathItem.__init__(self, parent=part_item.proxy()) self._viewroot = viewroot self._getActiveTool = part_item._getActiveTool self._controller = VirtualHelixItemController(self, self._model_part, False, True) self._handle = VirtualHelixHandleItem(self, part_item, viewroot) self._last_strand_set = None self._last_idx = None self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption) self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.setBrush(getNoBrush()) view = self.view() view.levelOfDetailChangedSignal.connect(self.levelOfDetailChangedSlot) should_show_details = view.shouldShowDetails() pen = newPenObj(styles.MINOR_GRID_STROKE, styles.MINOR_GRID_STROKE_WIDTH) pen.setCosmetic(should_show_details) self.setPen(pen) self.is_active = False self.refreshPath() self.setAcceptHoverEvents(True) # for pathtools self.setZValue(styles.ZPATHHELIX) self._right_mouse_move = False self.drag_last_position = self.handle_start = self.pos()
def __init__(self, **kwargs): """Summary Args: model_part (Part): The model part parent (TYPE): Description id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. key (None, optional): Description """ super().__init__(**kwargs) if self._key == "name": for vh in self.cnModelList(): self._controller_list.append( VirtualHelixItemController(self, vh.part(), True, False))
class VirtualHelixItem(QGraphicsPathItem): """VirtualHelixItem for PathView""" findChild = util.findChild # for debug def __init__(self, part_item, model_virtual_helix, viewroot): super(VirtualHelixItem, self).__init__(part_item.proxy()) self._part_item = part_item self._model_virtual_helix = model_virtual_helix self._viewroot = viewroot self._getActiveTool = part_item._getActiveTool self._controller = VirtualHelixItemController(self, model_virtual_helix) self._handle = VirtualHelixHandleItem(model_virtual_helix, part_item, viewroot) self._last_strand_set = None self._last_idx = None self._scaffoldBackground = None self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption) self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.setBrush(QBrush(Qt.NoBrush)) view = viewroot.scene().views()[0] view.levelOfDetailChangedSignal.connect(self.levelOfDetailChangedSlot) shouldShowDetails = view.shouldShowDetails() pen = QPen(styles.MINOR_GRID_STROKE, styles.MINOR_GRID_STROKE_WIDTH) pen.setCosmetic(shouldShowDetails) self.setPen(pen) self.refreshPath() self.setAcceptHoverEvents(True) # for pathtools self.setZValue(styles.ZPATHHELIX) # end def ### SIGNALS ### ### SLOTS ### def levelOfDetailChangedSlot(self, boolval): """Not connected to the model, only the QGraphicsView""" pen = self.pen() pen.setCosmetic(boolval) self.setPen(pen) # end def def strandAddedSlot(self, sender, strand): """ Instantiates a StrandItem upon notification that the model has a new Strand. The StrandItem is responsible for creating its own controller for communication with the model, and for adding itself to its parent (which is *this* VirtualHelixItem, i.e. 'self'). """ StrandItem(strand, self, self._viewroot) # end def def decoratorAddedSlot(self, decorator): """ Instantiates a DecoratorItem upon notification that the model has a new Decorator. The Decorator is responsible for creating its own controller for communication with the model, and for adding itself to its parent (which is *this* VirtualHelixItem, i.e. 'self'). """ pass def virtualHelixNumberChangedSlot(self, virtualHelix, number): self._handle.setNumber() # end def def virtualHelixRemovedSlot(self, virtualHelix): self._controller.disconnectSignals() self._controller = None scene = self.scene() self._handle.remove() scene.removeItem(self) self._part_item.removeVirtualHelixItem(self) self._part_item = None self._model_virtual_helix = None self._getActiveTool = None self._handle = None # end def ### ACCESSORS ### def coord(self): return self._model_virtual_helix.coord() # end def def viewroot(self): return self._viewroot # end def def handle(self): return self._handle # end def def part(self): return self._part_item.part() # end def def partItem(self): return self._part_item # end def def number(self): return self._model_virtual_helix.number() # end def def virtualHelix(self): return self._model_virtual_helix # end def def window(self): return self._part_item.window() # end def ### DRAWING METHODS ### def isStrandOnTop(self, strand): sS = strand.strandSet() isEvenParity = self._model_virtual_helix.isEvenParity() return isEvenParity and sS.isScaffold() or\ not isEvenParity and sS.isStaple() # end def def isStrandTypeOnTop(self, strand_type): isEvenParity = self._model_virtual_helix.isEvenParity() return isEvenParity and strand_type == StrandType.SCAFFOLD or \ not isEvenParity and strand_type == StrandType.STAPLE # end def def upperLeftCornerOfBase(self, idx, strand): x = idx * _BASE_WIDTH y = 0 if self.isStrandOnTop(strand) else _BASE_WIDTH return x, y # end def def upperLeftCornerOfBaseType(self, idx, strand_type): x = idx * _BASE_WIDTH y = 0 if self.isStrandTypeOnTop(strand_type) else _BASE_WIDTH return x, y # end def def refreshPath(self): """ Returns a QPainterPath object for the minor grid lines. The path also includes a border outline and a midline for dividing scaffold and staple bases. """ bw = _BASE_WIDTH bw2 = 2 * bw part = self.part() path = QPainterPath() sub_step_size = part.subStepSize() canvas_size = part.maxBaseIdx() + 1 # border path.addRect(0, 0, bw * canvas_size, 2 * bw) # minor tick marks for i in range(canvas_size): x = round(bw * i) + .5 if i % sub_step_size == 0: path.moveTo(x - .5, 0) path.lineTo(x - .5, bw2) path.lineTo(x - .25, bw2) path.lineTo(x - .25, 0) path.lineTo(x, 0) path.lineTo(x, bw2) path.lineTo(x + .25, bw2) path.lineTo(x + .25, 0) path.lineTo(x + .5, 0) path.lineTo(x + .5, bw2) # path.moveTo(x-.5, 0) # path.lineTo(x-.5, 2 * bw) # path.lineTo(x+.5, 2 * bw) # path.lineTo(x+.5, 0) else: path.moveTo(x, 0) path.lineTo(x, 2 * bw) # staple-scaffold divider path.moveTo(0, bw) path.lineTo(bw * canvas_size, bw) self.setPath(path) if self._model_virtual_helix.scaffoldIsOnTop(): scaffoldY = 0 else: scaffoldY = bw # if self._scaffoldBackground == None: # highlightr = QGraphicsRectItem(0, scaffoldY, bw * canvas_size, bw, self) # highlightr.setBrush(QBrush(styles.scaffold_bkg_fill)) # highlightr.setPen(QPen(Qt.NoPen)) # highlightr.setFlag(QGraphicsItem.ItemStacksBehindParent) # self._scaffoldBackground = highlightr # else: # self._scaffoldBackground.setRect(0, scaffoldY, bw * canvas_size, bw) # end def def resize(self): """Called by part on resize.""" self.refreshPath() ### PUBLIC SUPPORT METHODS ### def setActive(self, idx): """Makes active the virtual helix associated with this item.""" self.part().setActiveVirtualHelix(self._model_virtual_helix, idx) # end def ### EVENT HANDLERS ### def mousePressEvent(self, event): """ Parses a mousePressEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. """ self.scene().views()[0].addToPressList(self) strand_set, idx = self.baseAtPoint(event.pos()) self.setActive(idx) tool_method_name = self._getActiveTool().methodPrefix() + "MousePress" ### uncomment for debugging modifier selection # strand_set, idx = self.baseAtPoint(event.pos()) # row, col = strand_set.virtualHelix().coord() # self._part_item.part().selectPreDecorator([(row,col,idx)]) if hasattr(self, tool_method_name): self._last_strand_set, self._last_idx = strand_set, idx getattr(self, tool_method_name)(strand_set, idx) else: event.setAccepted(False) # end def def mouseMoveEvent(self, event): """ Parses a mouseMoveEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. """ tool_method_name = self._getActiveTool().methodPrefix() + "MouseMove" if hasattr(self, tool_method_name): strand_set, idx = self.baseAtPoint(event.pos()) if self._last_strand_set != strand_set or self._last_idx != idx: self._last_strand_set, self._last_idx = strand_set, idx getattr(self, tool_method_name)(strand_set, idx) else: event.setAccepted(False) # end def def customMouseRelease(self, event): """ Parses a mouseReleaseEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. """ tool_method_name = self._getActiveTool().methodPrefix() + "MouseRelease" if hasattr(self, tool_method_name): getattr(self, tool_method_name)(self._last_strand_set, self._last_idx) else: event.setAccepted(False) # end def ### COORDINATE UTILITIES ### def baseAtPoint(self, pos): """ Returns the (strand_type, index) under the location x,y or None. It shouldn't be possible to click outside a pathhelix and still call this function. However, this sometimes happens if you click exactly on the top or bottom edge, resulting in a negative y value. """ x, y = pos.x(), pos.y() mVH = self._model_virtual_helix base_idx = int(floor(x / _BASE_WIDTH)) min_base, max_base = 0, mVH.part().maxBaseIdx() if base_idx < min_base or base_idx >= max_base: base_idx = util.clamp(base_idx, min_base, max_base) if y < 0: y = 0 # HACK: zero out y due to erroneous click strandIdx = floor(y * 1. / _BASE_WIDTH) if strandIdx < 0 or strandIdx > 1: strandIdx = int(util.clamp(strandIdx, 0, 1)) strand_set = mVH.getStrandSetByIdx(strandIdx) return (strand_set, base_idx) # end def def keyPanDeltaX(self): """How far a single press of the left or right arrow key should move the scene (in scene space)""" dx = self._part_item.part().stepSize() * _BASE_WIDTH return self.mapToScene(QRectF(0, 0, dx, 1)).boundingRect().width() # end def def hoverLeaveEvent(self, event): self._part_item.updateStatusBar("") # end def def hoverMoveEvent(self, event): """ Parses a mouseMoveEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. """ base_idx = int(floor(event.pos().x() / _BASE_WIDTH)) loc = "%d[%d]" % (self.number(), base_idx) self._part_item.updateStatusBar(loc) active_tool = self._getActiveTool() tool_method_name = self._getActiveTool().methodPrefix() + "HoverMove" if hasattr(self, tool_method_name): strand_type, idx_x, idx_y = active_tool.baseAtPoint(self, event.pos()) getattr(self, tool_method_name)(strand_type, idx_x, idx_y) # end def ### TOOL METHODS ### def pencilToolMousePress(self, strand_set, idx): """strand.getDragBounds""" # print "%s: %s[%s]" % (util.methodName(), strand_set, idx) active_tool = self._getActiveTool() if not active_tool.isDrawingStrand(): active_tool.initStrandItemFromVHI(self, strand_set, idx) active_tool.setIsDrawingStrand(True) # end def def pencilToolMouseMove(self, strand_set, idx): """strand.getDragBounds""" # print "%s: %s[%s]" % (util.methodName(), strand_set, idx) active_tool = self._getActiveTool() if active_tool.isDrawingStrand(): active_tool.updateStrandItemFromVHI(self, strand_set, idx) # end def def pencilToolMouseRelease(self, strand_set, idx): """strand.getDragBounds""" # print "%s: %s[%s]" % (util.methodName(), strand_set, idx) active_tool = self._getActiveTool() if active_tool.isDrawingStrand(): active_tool.setIsDrawingStrand(False) active_tool.attemptToCreateStrand(self, strand_set, idx) # end def def pencilToolHoverMove(self, strand_type, idx_x, idx_y): """Pencil the strand is possible.""" part_item = self.partItem() active_tool = self._getActiveTool() if not active_tool.isFloatingXoverBegin(): temp_xover = active_tool.floatingXover() temp_xover.updateFloatingFromVHI(self, strand_type, idx_x, idx_y)
class VirtualHelixItem(QGraphicsEllipseItem): """ The VirtualHelixItem is an individual circle that gets drawn in the SliceView as a child of the PartItem. Taken as a group, many SliceHelix instances make up the crossection of the DNAPart. Clicking on a SliceHelix adds a VirtualHelix to the DNAPart. The SliceHelix then changes appearence and paints its corresponding VirtualHelix number. """ # set up default, hover, and active drawing styles _USE_BRUSH = QBrush(styles.ORANGE_FILL) _USE_PEN = QPen(styles.ORANGE_STROKE, styles.SLICE_HELIX_STROKE_WIDTH) _RADIUS = styles.SLICE_HELIX_RADIUS _OUT_OF_SLICE_PEN = QPen(styles.LIGHT_ORANGE_STROKE,\ styles.SLICE_HELIX_STROKE_WIDTH) _OUT_OF_SLICE_BRUSH = QBrush(styles.LIGHT_ORANGE_FILL) _RECT = QRectF(0, 0, 2 * _RADIUS, 2 * _RADIUS) _FONT = styles.SLICE_NUM_FONT _ZVALUE = styles.ZSLICEHELIX+3 def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = empty_helix_item self.hide() # drawing related self.isHovered = False self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(self._ZVALUE) self.lastMousePressAddedBases = False self.setBrush(self._OUT_OF_SLICE_BRUSH) self.setPen(self._OUT_OF_SLICE_PEN) self.setRect(self._RECT) # handle the label specific stuff self._label = self.createLabel() self.setNumber() self.createArrow() self._controller = VirtualHelixItemController(self, model_virtual_helix) self.show() # end def ### SIGNALS ### ### SLOTS ### def virtualHelixNumberChangedSlot(self, virtualHelix): """ receives a signal containing a virtualHelix and the oldNumber as a safety check """ self.setNumber() # end def def virtualHelixRemovedSlot(self, virtualHelix): self._controller.disconnectSignals() self._controller = None self._empty_helix_item.setNotHovered() self._virtual_helix = None self._empty_helix_item = None self.scene().removeItem(self._label) self._label = None self.scene().removeItem(self) # end def def strandAddedSlot(self, sender, strand): pass # end def ### def createLabel(self): label = QGraphicsSimpleTextItem("%d" % self._virtual_helix.number()) label.setFont(self._FONT) label.setZValue(self._ZVALUE) label.setParentItem(self) return label # end def def createArrow(self): rad = self._RADIUS pen = QPen() pen.setWidth(3) color = QColor(Qt.blue) color.setAlphaF(0.25) pen.setBrush(color) if self._virtual_helix.isEvenParity(): arrow = QGraphicsLineItem(rad, rad, 2*rad, rad, self) else: arrow = QGraphicsLineItem(0, rad, rad, rad, self) arrow.setTransformOriginPoint(rad, rad) arrow.setZValue(400) arrow.setPen(pen) self.arrow = arrow self.arrow.hide() # end def def updateArrow(self, idx): part = self.part() tpb = part._TWIST_PER_BASE angle = idx*tpb # for some reason rotation is CW and not CCW with increasing angle self.arrow.setRotation(angle + part._TWIST_OFFSET) # end def def setNumber(self): """docstring for setNumber""" vh = self._virtual_helix num = vh.number() label = self._label radius = self._RADIUS if num != None: label.setText("%d" % num) else: return y_val = radius / 3 if num < 10: label.setPos(radius / 1.5, y_val) elif num < 100: label.setPos(radius / 3, y_val) else: # _number >= 100 label.setPos(0, y_val) b_rect = label.boundingRect() posx = b_rect.width()/2 posy = b_rect.height()/2 label.setPos(radius-posx, radius-posy) # end def def part(self): return self._empty_helix_item.part() def virtualHelix(self): return self._virtual_helix # end def def number(self): return self.virtualHelix().number() def setActiveSliceView(self, is_active_now, idx): if is_active_now: self.setPen(self._USE_PEN) self.setBrush(self._USE_BRUSH) self.updateArrow(idx) self.arrow.show() else: self.setPen(self._OUT_OF_SLICE_PEN) self.setBrush(self._OUT_OF_SLICE_BRUSH) self.arrow.hide() # end def ############################ User Interaction ############################ def sceneEvent(self, event): """Included for unit testing in order to grab events that are sent via QGraphicsScene.sendEvent().""" # if self._parent.sliceController.testRecorder: # coord = (self._row, self._col) # self._parent.sliceController.testRecorder.sliceSceneEvent(event, coord) if event.type() == QEvent.MouseButtonPress: self.mousePressEvent(event) return True elif event.type() == QEvent.MouseButtonRelease: self.mouseReleaseEvent(event) return True elif event.type() == QEvent.MouseMove: self.mouseMoveEvent(event) return True QGraphicsItem.sceneEvent(self, event) return False def hoverEnterEvent(self, event): """ If the selection is configured to always select everything, we don't draw a focus ring around everything, instead we only draw a focus ring around the hovered obj. """ # if self.selectAllBehavior(): # self.setSelected(True) # forward the event to the empty_helix_item as well self._empty_helix_item.hoverEnterEvent(event) # end def def hoverLeaveEvent(self, event): # if self.selectAllBehavior(): # self.setSelected(False) self._empty_helix_item.hoverEnterEvent(event)
class VirtualHelixItem(QGraphicsEllipseItem, AbstractVirtualHelixItem): """ The VirtualHelixItem is an individual circle that gets drawn in the SliceView as a child of the OrigamiPartItem. Taken as a group, many SliceHelix instances make up the crossection of the PlasmidPart. Clicking on a SliceHelix adds a VirtualHelix to the PlasmidPart. The SliceHelix then changes appearence and paints its corresponding VirtualHelix number. """ def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = ehi = empty_helix_item self._controller = VirtualHelixItemController(self, model_virtual_helix) self.hide() self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(_ZVALUE) self.lastMousePressAddedBases = False # handle the label specific stuff self._label = self.createLabel() self.setNumber() self._pen1, self._pen2 = (QPen(), QPen()) self.createArrows() self.updateAppearance() self._prexoveritemgroup = _pxig = PreXoverItemGroup(_RECT, self) _pxig.setTransformOriginPoint(_RECT.center()) # self._rect = QRectF(_RECT) # self._hover_rect = QRectF(_RECT) # self._outer_line = RotaryDialLine(self._rect, self) # self._hover_region = RotaryDialHoverRegion(self._hover_rect, self) self.show() # self._virtual_helix.setProperty('ehiX', ehi.mapToScene(0,0).x()) # self._virtual_helix.setProperty('ehiY', ehi.mapToScene(0,0).y()) self.getNeighbors() # end def def getNeighbors(self): items = filter(lambda x: type(x) is VirtualHelixItem, self.collidingItems()) name = self._virtual_helix.getName() pos = self.scenePos() print("\n%s (%d, %d)" % (name, pos.x(), pos.y())) for nvhi in items: npos = nvhi.scenePos() line = QLineF(pos, npos) print("\t%s (%d, %d) %d" % (nvhi.virtualHelix().getName(), npos.x(), npos.y(), line.length())) def modelColor(self): return self.part().getProperty('color') # end def def updateAppearance(self): part_color = self.part().getProperty('color') self._USE_PEN = getPenObj(part_color, styles.SLICE_HELIX_STROKE_WIDTH) self._OUT_OF_SLICE_PEN = getPenObj(part_color, styles.SLICE_HELIX_STROKE_WIDTH) self._USE_BRUSH = getBrushObj(part_color, alpha=128) self._OUT_OF_SLICE_BRUSH = getBrushObj(part_color, alpha=64) self._OUT_OF_SLICE_TEXT_BRUSH = getBrushObj(styles.OUT_OF_SLICE_TEXT_COLOR) if self.part().crossSectionType() == LatticeType.HONEYCOMB: self._USE_PEN = getPenObj(styles.BLUE_STROKE, styles.SLICE_HELIX_STROKE_WIDTH) self._OUT_OF_SLICE_PEN = getPenObj(styles.BLUE_STROKE,\ styles.SLICE_HELIX_STROKE_WIDTH) if self.part().partType() == PartType.NUCLEICACIDPART: self._OUT_OF_SLICE_BRUSH = _OUT_OF_SLICE_BRUSH_DEFAULT # self._USE_BRUSH = getBrushObj(part_color, lighter=180) self._USE_BRUSH = getBrushObj(part_color, alpha=150) self._label.setBrush(self._OUT_OF_SLICE_TEXT_BRUSH) self.setBrush(self._OUT_OF_SLICE_BRUSH) self.setPen(self._OUT_OF_SLICE_PEN) self.setRect(_RECT) # end def ### SIGNALS ### ### SLOTS ### def virtualHelixNumberChangedSlot(self, virtual_helix): """ receives a signal containing a virtual_helix and the oldNumber as a safety check """ self.setNumber() # end def def virtualHelixPropertyChangedSlot(self, virtual_helix, property_key, new_value): if property_key == 'eulerZ': self._prexoveritemgroup.setRotation(new_value) scamZ = self._virtual_helix.getProperty('scamZ') if scamZ != new_value: self._virtual_helix.setProperty('scamZ', new_value) elif property_key == 'scamZ': eulerZ = self._virtual_helix.getProperty('eulerZ') if eulerZ != new_value: self._virtual_helix.setProperty('eulerZ', new_value) elif property_key == 'ehiX': ehi_pos = self._empty_helix_item.scenePos() self._empty_helix_item.setPos(new_value, ehi_pos.y()) pass elif property_key == 'ehiY': ehi_pos = self._empty_helix_item.scenePos() self._empty_helix_item.setPos(ehi_pos.x(),new_value) # end def def virtualHelixRemovedSlot(self, virtual_helix): self._controller.disconnectSignals() self._controller = None self._empty_helix_item.setNotHovered() self._virtual_helix = None self._empty_helix_item = None self.scene().removeItem(self._label) self._label = None self.scene().removeItem(self) # end def def createLabel(self): label = QGraphicsSimpleTextItem("%d" % self._virtual_helix.number()) label.setFont(_FONT) label.setZValue(_ZVALUE) label.setParentItem(self) return label # end def def createArrows(self): rad = _RADIUS pen1 = self._pen1 pen2 = self._pen2 pen1.setWidth(3) pen2.setWidth(3) pen1.setBrush(Qt.gray) pen2.setBrush(Qt.lightGray) if self._virtual_helix.isEvenParity(): arrow1 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow2 = QGraphicsLineItem(0, rad, rad, rad, self) else: arrow1 = QGraphicsLineItem(0, rad, rad, rad, self) arrow2 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow1.setTransformOriginPoint(rad, rad) arrow2.setTransformOriginPoint(rad, rad) arrow1.setZValue(400) arrow2.setZValue(400) arrow1.setPen(pen1) arrow2.setPen(pen2) self.arrow1 = arrow1 self.arrow2 = arrow2 self.arrow1.hide() self.arrow2.hide() # end def def updateScafArrow(self, idx): scaf_strand = self._virtual_helix.scaf(idx) if scaf_strand: scaf_strand_color = scaf_strand.oligo().getColor() scaf_alpha = 230 if scaf_strand.hasXoverAt(idx) else 128 else: scaf_strand_color = '#a0a0a4' #Qt.gray scaf_alpha = 26 scaf_strand_color_obj = getColorObj(scaf_strand_color, alpha=scaf_alpha) self._pen1.setBrush(scaf_strand_color_obj) self.arrow1.setPen(self._pen1) part = self.part() tpb = part._TWIST_PER_BASE angle = idx*tpb # for some reason rotation is CW and not CCW with increasing angle self.arrow1.setRotation(angle + part._TWIST_OFFSET) def updateStapArrow(self, idx): stap_strand = self._virtual_helix.stap(idx) if stap_strand: stap_strand_color = stap_strand.oligo().getColor() stap_alpha = 230 if stap_strand.hasXoverAt(idx) else 128 else: stap_strand_color = '#c0c0c0' # Qt.lightGray stap_alpha = 26 stap_strand_color_obj = getColorObj(stap_strand_color, alpha=stap_alpha) self._pen2.setBrush(stap_strand_color_obj) self.arrow2.setPen(self._pen2) part = self.part() tpb = part._TWIST_PER_BASE angle = idx*tpb self.arrow2.setRotation(angle + part._TWIST_OFFSET) # end def def setNumber(self): """docstring for setNumber""" vh = self._virtual_helix num = vh.number() label = self._label if num is not None: label.setText("%d" % num) else: return y_val = _RADIUS / 3 if num < 10: label.setPos(_RADIUS / 1.5, y_val) elif num < 100: label.setPos(_RADIUS / 3, y_val) else: # _number >= 100 label.setPos(0, y_val) b_rect = label.boundingRect() posx = b_rect.width()/2 posy = b_rect.height()/2 label.setPos(_RADIUS-posx, _RADIUS-posy) # end def def part(self): return self._empty_helix_item.part() def virtualHelix(self): return self._virtual_helix # end def def number(self): return self.virtualHelix().number() def setActiveSliceView(self, idx, has_scaf, has_stap): if has_scaf: self.setPen(self._USE_PEN) self.setBrush(self._USE_BRUSH) self._label.setBrush(_USE_TEXT_BRUSH) self.updateScafArrow(idx) self.arrow1.show() else: self.setPen(self._OUT_OF_SLICE_PEN) self.setBrush(self._OUT_OF_SLICE_BRUSH) self._label.setBrush(self._OUT_OF_SLICE_TEXT_BRUSH) self.arrow1.hide() if has_stap: self.updateStapArrow(idx) self.arrow2.show() else: self.arrow2.hide() # end def ############################ User Interaction ############################ def sceneEvent(self, event): """Included for unit testing in order to grab events that are sent via QGraphicsScene.sendEvent().""" # if self._parent.sliceController.testRecorder: # coord = (self._row, self._col) # self._parent.sliceController.testRecorder.sliceSceneEvent(event, coord) if event.type() == QEvent.MouseButtonPress: self.mousePressEvent(event) return True elif event.type() == QEvent.MouseButtonRelease: self.mouseReleaseEvent(event) return True elif event.type() == QEvent.MouseMove: self.mouseMoveEvent(event) return True QGraphicsItem.sceneEvent(self, event) return False def hoverEnterEvent(self, event): """ If the selection is configured to always select everything, we don't draw a focus ring around everything, instead we only draw a focus ring around the hovered obj. """ # if self.selectAllBehavior(): # self.setSelected(True) # forward the event to the empty_helix_item as well self._empty_helix_item.hoverEnterEvent(event) # end def def hoverLeaveEvent(self, event): # if self.selectAllBehavior(): # self.setSelected(False) self._empty_helix_item.hoverEnterEvent(event)
class VirtualHelixItem(QGraphicsEllipseItem, AbstractVirtualHelixItem): """ The VirtualHelixItem is an individual circle that gets drawn in the SliceView as a child of the OrigamiPartItem. Taken as a group, many SliceHelix instances make up the crossection of the PlasmidPart. Clicking on a SliceHelix adds a VirtualHelix to the PlasmidPart. The SliceHelix then changes appearence and paints its corresponding VirtualHelix number. """ # set up default, hover, and active drawing styles _RADIUS = styles.SLICE_HELIX_RADIUS _RECT = QRectF(0, 0, 2 * _RADIUS, 2 * _RADIUS) rect_gain = 0.25 _RECT = _RECT.adjusted(rect_gain, rect_gain, rect_gain, rect_gain) _FONT = styles.SLICE_NUM_FONT _ZVALUE = styles.ZSLICEHELIX+3 _OUT_OF_SLICE_BRUSH_DEFAULT = getBrushObj(styles.OUT_OF_SLICE_FILL) # QBrush(QColor(250, 250, 250)) _USE_TEXT_BRUSH = getBrushObj(styles.USE_TEXT_COLOR) _OUT_OF_SLICE_TEXT_BRUSH = getBrushObj(styles.OUT_OF_SLICE_TEXT_COLOR) def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = empty_helix_item self._controller = VirtualHelixItemController(self, model_virtual_helix) self.hide() # drawing related self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(self._ZVALUE) self.lastMousePressAddedBases = False # handle the label specific stuff self._label = self.createLabel() self.setNumber() self._pen1, self._pen2 = (QPen(), QPen()) self.createArrows() self.updateProperty() self._rect = QRectF() self._hover_rect = QRectF() self._outer_line = RotateLine(self._rect, self) # self._inner_line = RotateLine(self._rect, self) self._hover_region = RotateHoverRegion(self._hover_rect, self) self.updateRects() self.show() # end def def radius(self): return self._radius # end def def modelColor(self): return self.part().getProperty('color') # end def def updateRects(self): diameter = self.boundingRect().width()/2 # round(dna_length * pi / 100,2) self._radius = self._RADIUS diameter = self._RADIUS * 2 self._rect = QRectF(0, 0, diameter, diameter) self._outer_line.updateRect(QRectF(-GAP/2, -GAP/2, diameter+GAP, diameter+GAP)) # self._inner_line.updateRect(QRectF(GAP/2, GAP/2, diameter-GAP, diameter-GAP)) # self._hover_rect = self._rect.adjusted(-H_W, -H_W, H_W, H_W) self._hover_region.updateRect(self._rect) # end def def updateProperty(self): part_color = self.part().getProperty('color') self._USE_PEN = getPenObj(part_color, styles.SLICE_HELIX_STROKE_WIDTH) self._OUT_OF_SLICE_PEN = getPenObj(part_color, styles.SLICE_HELIX_STROKE_WIDTH) self._USE_BRUSH = getBrushObj(part_color, alpha=128) self._OUT_OF_SLICE_BRUSH = getBrushObj(part_color, alpha=64) if self.part().crossSectionType() == LatticeType.HONEYCOMB: self._USE_PEN = getPenObj(styles.BLUE_STROKE, styles.SLICE_HELIX_STROKE_WIDTH) self._OUT_OF_SLICE_PEN = getPenObj(styles.BLUE_STROKE,\ styles.SLICE_HELIX_STROKE_WIDTH) if self.part().partType() == PartType.NUCLEICACIDPART: self._OUT_OF_SLICE_BRUSH = self._OUT_OF_SLICE_BRUSH_DEFAULT # self._USE_BRUSH = getBrushObj(part_color, lighter=180) self._USE_BRUSH = getBrushObj(part_color, alpha=150) self._label.setBrush(self._OUT_OF_SLICE_TEXT_BRUSH) self.setBrush(self._OUT_OF_SLICE_BRUSH) self.setPen(self._OUT_OF_SLICE_PEN) self.setRect(self._RECT) # end def ### SIGNALS ### ### SLOTS ### def virtualHelixNumberChangedSlot(self, virtual_helix): """ receives a signal containing a virtual_helix and the oldNumber as a safety check """ self.setNumber() # end def def virtualHelixPropertyChangedSlot(self, virtual_helix, property_key, new_value): if property_key == 'eulerZ': self.setTransformOriginPoint(self.boundingRect().center()) self.setRotation(new_value) self.setTransformOriginPoint(QPointF(0,0)) # end def def virtualHelixRemovedSlot(self, virtual_helix): self._controller.disconnectSignals() self._controller = None self._empty_helix_item.setNotHovered() self._virtual_helix = None self._empty_helix_item = None self.scene().removeItem(self._label) self._label = None self.scene().removeItem(self) # end def def createLabel(self): label = QGraphicsSimpleTextItem("%d" % self._virtual_helix.number()) label.setFont(self._FONT) label.setZValue(self._ZVALUE) label.setParentItem(self) return label # end def def createArrows(self): rad = self._RADIUS pen1 = self._pen1 pen2 = self._pen2 pen1.setWidth(3) pen2.setWidth(3) pen1.setBrush(Qt.gray) pen2.setBrush(Qt.lightGray) if self._virtual_helix.isEvenParity(): arrow1 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow2 = QGraphicsLineItem(0, rad, rad, rad, self) else: arrow1 = QGraphicsLineItem(0, rad, rad, rad, self) arrow2 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow1.setTransformOriginPoint(rad, rad) arrow2.setTransformOriginPoint(rad, rad) arrow1.setZValue(400) arrow2.setZValue(400) arrow1.setPen(pen1) arrow2.setPen(pen2) self.arrow1 = arrow1 self.arrow2 = arrow2 self.arrow1.hide() self.arrow2.hide() # end def def updateScafArrow(self, idx): scaf_strand = self._virtual_helix.scaf(idx) if scaf_strand: scaf_strand_color = scaf_strand.oligo().color() scaf_alpha = 230 if scaf_strand.hasXoverAt(idx) else 128 else: scaf_strand_color = '#a0a0a4' #Qt.gray scaf_alpha = 26 scaf_strand_color_obj = getColorObj(scaf_strand_color, alpha=scaf_alpha) self._pen1.setBrush(scaf_strand_color_obj) self.arrow1.setPen(self._pen1) part = self.part() tpb = part._TWIST_PER_BASE angle = idx*tpb # for some reason rotation is CW and not CCW with increasing angle self.arrow1.setRotation(angle + part._TWIST_OFFSET) def updateStapArrow(self, idx): stap_strand = self._virtual_helix.stap(idx) if stap_strand: stap_strand_color = stap_strand.oligo().color() stap_alpha = 230 if stap_strand.hasXoverAt(idx) else 128 else: stap_strand_color = '#c0c0c0' # Qt.lightGray stap_alpha = 26 stap_strand_color_obj = getColorObj(stap_strand_color, alpha=stap_alpha) self._pen2.setBrush(stap_strand_color_obj) self.arrow2.setPen(self._pen2) part = self.part() tpb = part._TWIST_PER_BASE angle = idx*tpb self.arrow2.setRotation(angle + part._TWIST_OFFSET) # end def def setNumber(self): """docstring for setNumber""" vh = self._virtual_helix num = vh.number() label = self._label radius = self._RADIUS if num is not None: label.setText("%d" % num) else: return y_val = radius / 3 if num < 10: label.setPos(radius / 1.5, y_val) elif num < 100: label.setPos(radius / 3, y_val) else: # _number >= 100 label.setPos(0, y_val) b_rect = label.boundingRect() posx = b_rect.width()/2 posy = b_rect.height()/2 label.setPos(radius-posx, radius-posy) # end def def part(self): return self._empty_helix_item.part() def virtualHelix(self): return self._virtual_helix # end def def number(self): return self.virtualHelix().number() def setActiveSliceView(self, idx, has_scaf, has_stap): if has_scaf: self.setPen(self._USE_PEN) self.setBrush(self._USE_BRUSH) self._label.setBrush(self._USE_TEXT_BRUSH) self.updateScafArrow(idx) self.arrow1.show() else: self.setPen(self._OUT_OF_SLICE_PEN) self.setBrush(self._OUT_OF_SLICE_BRUSH) self._label.setBrush(self._OUT_OF_SLICE_TEXT_BRUSH) self.arrow1.hide() if has_stap: self.updateStapArrow(idx) self.arrow2.show() else: self.arrow2.hide() # end def ############################ User Interaction ############################ def sceneEvent(self, event): """Included for unit testing in order to grab events that are sent via QGraphicsScene.sendEvent().""" # if self._parent.sliceController.testRecorder: # coord = (self._row, self._col) # self._parent.sliceController.testRecorder.sliceSceneEvent(event, coord) if event.type() == QEvent.MouseButtonPress: self.mousePressEvent(event) return True elif event.type() == QEvent.MouseButtonRelease: self.mouseReleaseEvent(event) return True elif event.type() == QEvent.MouseMove: self.mouseMoveEvent(event) return True QGraphicsItem.sceneEvent(self, event) return False def hoverEnterEvent(self, event): """ If the selection is configured to always select everything, we don't draw a focus ring around everything, instead we only draw a focus ring around the hovered obj. """ # if self.selectAllBehavior(): # self.setSelected(True) # forward the event to the empty_helix_item as well self._empty_helix_item.hoverEnterEvent(event) # end def def hoverLeaveEvent(self, event): # if self.selectAllBehavior(): # self.setSelected(False) self._empty_helix_item.hoverEnterEvent(event)
class SliceVirtualHelixItem(AbstractVirtualHelixItem, QGraphicsEllipseItem): """The VirtualHelixItem is an individual circle that gets drawn in the SliceView as a child of the NucleicAcidPartItem. Taken as a group, many SliceHelix instances make up the crossection of the NucleicAcidPart. Clicking on a SliceHelix adds a VirtualHelix to the PlasmidPart. The SliceHelix then changes appearence and paints its corresponding VirtualHelix number. Attributes: FILTER_NAME (str): Belongs to the filter class 'virtual_helix'. is_active (bool): Does the item have focus. old_pen (QPen): temp storage for pen for easy restoration on appearance change. wedge_gizmos (dict): dictionary of `WedgeGizmo` objects. """ FILTER_NAME = 'virtual_helix' def __init__(self, model_virtual_helix, part_item): """ Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. part_item (cadnano.gui.views.sliceview.nucleicacidpartitem.NucleicAcidPartItem): the part item """ AbstractVirtualHelixItem.__init__(self, model_virtual_helix, part_item) QGraphicsEllipseItem.__init__(self, parent=part_item) self._controller = VirtualHelixItemController(self, self._model_part, False, True) self.hide() model_part = self._model_part x, y = model_part.locationQt(self._id_num, part_item.scaleFactor()) # set position to offset for radius # self.setTransformOriginPoint(_RADIUS, _RADIUS) self.setCenterPos(x, y) self.wedge_gizmos = {} self._added_wedge_gizmos = set() # self._prexo_gizmos = [] self.setAcceptHoverEvents(True) self.setZValue(_ZVALUE) # handle the label specific stuff self._label = self.createLabel() self.setNumber() self.old_pen = None self.is_active = False self.updateAppearance() self.show() self._right_mouse_move = False # end def ### ACCESSORS ### def part(self): """Reference to the model part associated with the parent part item. Returns: NucleicAcidPart: the model part """ return self._part_item.part() # end def def setSnapOrigin(self, is_snap): """Used to toggle an item as the snap origin. See `SelectSliceTool`. Args: is_snap (bool): True if this should be the snap origin, False otherwise. """ if is_snap: op = self.pen() if self.old_pen is None: self.old_pen = op self.setPen(getPenObj(op.color().name(), SNAP_WIDTH)) else: self.setPen(self.old_pen) self.old_pen = None def partItem(self): """Reference to the parent part item. Returns: NucleicAcidPartItem: the part item """ return self._part_item # end def def idNum(self): """Virual helix ID number. Returns: int: the id_num of the virtual helix object. """ return self._id_num # end def def activate(self): """Sets the VirtualHelixItem object as active (i.e. having focus due to user input) and calls updateAppearance. """ self.is_active = True self.updateAppearance() def deactivate(self): """Sets the VirtualHelixItem object as not active (i.e. does not have focus) and calls updateAppearance. """ self.is_active = False self.updateAppearance() def setCenterPos(self, x, y): """Moves this item a new position such that its center is located at (x,y). Args: x (float): new x coordinate y (float): new y coordinate """ # invert the y axis part_item = self._part_item parent_item = self.parentItem() pos = QPointF(x - _RADIUS, y - _RADIUS) if parent_item != part_item: pos = parent_item.mapFromItem(part_item, pos) self.setPos(pos) # end def def getCenterScenePos(self): """ Returns: QPointF: the scenePos of the virtualhelixitem center """ return self.scenePos() + QPointF(_RADIUS, _RADIUS) # end def def modelColor(self): """The color associated with this item's `Part`. Returns: str: hex representation of Color, e.g. `#0066cc`. """ return self._model_part.getProperty('color') # end def def partCrossoverSpanAngle(self): """The angle span, centered at each base, within which crossovers will be allowed by the interface. The span is drawn by the WedgeGizmo. Returns: int: The span angle (default is set in NucleicAcidPart init) """ return float(self._model_part.getProperty('crossover_span_angle')) # end def ### SIGNALS ### ### SLOTS ### def mousePressEvent(self, event): """Event handler for when the mouse button is pressed inside this item. If a tool-specific mouse press method is defined, it will be called for the currently active tool. Otherwise, the default QGraphicsItem.mousePressEvent will be called. Note: Only applies the event if the clicked item is in the part item's active filter set. Args: event (QMouseEvent): contains parameters that describe the mouse event. """ if self.FILTER_NAME not in self._part_item.getFilterSet(): return if event.button() == Qt.RightButton: return part_item = self._part_item tool = part_item._getActiveTool() tool_method_name = tool.methodPrefix() + "MousePress" if hasattr(self, tool_method_name): getattr(self, tool_method_name)(tool, part_item, event) else: QGraphicsItem.mousePressEvent(self, event) # end def def selectToolMousePress(self, tool, part_item, event): """The event handler for when the mouse button is pressed inside this item with the SelectTool active. Args: tool (SelectSliceTool): reference to call tool-specific methods part_item (cadnano.gui.views.sliceview.nucleicacidpartitem.NucleicAcidPartItem): reference to the part item event (QMouseEvent): contains parameters that describe the mouse event """ part = self._model_part part.setSelected(True) tool.selectOrSnap(part_item, self, event) # return QGraphicsItem.mousePressEvent(self, event) # end def def pencilToolMousePress(self, tool, part_item, event): """Summary Args: tool (SelectSliceTool): reference to call tool-specific methods part_item (cadnano.gui.views.sliceview.nucleicacidpartitem.NucleicAcidPartItem): reference to the part item event (QMouseEvent): contains parameters that describe the mouse event """ part = self._model_part print("pencilToolMousePress", part) # tool.attemptToCreateStrand def virtualHelixPropertyChangedSlot(self, keys, values): """The event handler for when the a model virtual helix propety has changed. See partVirtualHelixPropertyChangedSignal. Calls updateAppearance(), which will refresh styles if needed. Args: keys (tuple): names of properties that changed values (tuple): new values of properties that change """ # for key, val in zip(keys, values): # pass self.updateAppearance() # end def def virtualHelixRemovedSlot(self): """The event handler for when a virtual helix is removed from the model. Disconnects signals, and sets internal references to label, part_item, and model_part to None, and finally removes the item from the scene. """ self._controller.disconnectSignals() self._controller = None part_item = self._part_item tool = part_item._getActiveTool() if tool.methodPrefix() == "selectTool": tool.hideLineItem() self.scene().removeItem(self._label) self._label = None self._part_item = None self._model_part = None self.scene().removeItem(self) # end def def updateAppearance(self): """Check item's current visibility, color and active state, and sets pen, brush, text according to style defaults. """ is_visible, color = self._model_vh.getProperty(['is_visible', 'color']) if is_visible: self.show() else: self.hide() return pwidth = styles.SLICE_HELIX_STROKE_WIDTH if self.old_pen is None else SNAP_WIDTH if self.is_active: self._USE_PEN = getPenObj(styles.ACTIVE_STROKE, pwidth) else: self._USE_PEN = getPenObj(color, pwidth) self._TEXT_BRUSH = getBrushObj(styles.SLICE_TEXT_COLOR) self._BRUSH = _BRUSH_DEFAULT self._USE_BRUSH = getBrushObj(color, alpha=150) self._label.setBrush(self._TEXT_BRUSH) self.setBrush(self._BRUSH) self.setPen(self._USE_PEN) self.setRect(_RECT) # end def def updatePosition(self): """Queries the model virtual helix for latest x,y coodinates, and sets them in the scene if necessary. Note: coordinates in the model are always in the part """ part_item = self._part_item # sf = part_item.scaleFactor() x, y = self._model_part.locationQt(self._id_num, part_item.scaleFactor()) new_pos = QPointF(x - _RADIUS, y - _RADIUS) # top left tl_pos = part_item.mapFromScene(self.scenePos()) # top left """ better to compare QPointF's since it handles difference tolerances for you with != """ if new_pos != tl_pos: parent_item = self.parentItem() if parent_item != part_item: new_pos = parent_item.mapFromItem(part_item, new_pos) self.setPos(new_pos) # end def def createLabel(self): """Creates a text label to display the ID number. Font and Z are set in slicestyles. Returns: QGraphicsSimpleTextItem: the label """ label = QGraphicsSimpleTextItem("%d" % self.idNum()) label.setFont(_FONT) label.setZValue(_ZVALUE) label.setParentItem(self) return label # end def def beginAddWedgeGizmos(self): """Resets the list of WedgeGizmos that will be processed by endAddWedgeGizmos. Called by NucleicAcidPartItem _refreshVirtualHelixItemGizmos, before setWedgeGizmo and endAddWedgeGizmos. """ self._added_wedge_gizmos.clear() # end def def endAddWedgeGizmos(self): """Removes beginAddWedgeGizmos that no longer point at a neighbor virtual helix. Called by NucleicAcidPartItem _refreshVirtualHelixItemGizmos in, after beginAddWedgeGizmos and setWedgeGizmo. """ remove_list = [] scene = self.scene() wg_dict = self.wedge_gizmos recently_added = self._added_wedge_gizmos for neighbor_virtual_helix in wg_dict.keys(): if neighbor_virtual_helix not in recently_added: remove_list.append(neighbor_virtual_helix) for nvh in remove_list: wg = wg_dict.get(nvh) del wg_dict[nvh] scene.removeItem(wg) # end def def setWedgeGizmo(self, neighbor_virtual_helix, neighbor_virtual_helix_item): """Adds a WedgeGizmo to oriented toward the specified neighbor vhi. Called by NucleicAcidPartItem _refreshVirtualHelixItemGizmos, in between with beginAddWedgeGizmos and endAddWedgeGizmos. Args: neighbor_virtual_helix (int): the id_num of neighboring virtual helix neighbor_virtual_helix_item (cadnano.gui.views.sliceview.virtualhelixitem.VirtualHelixItem): the neighboring virtual helix item """ wg_dict = self.wedge_gizmos nvhi = neighbor_virtual_helix_item nvhi_name = nvhi.cnModel().getProperty('name') pos = self.scenePos() line = QLineF(pos, nvhi.scenePos()) line.translate(_RADIUS, _RADIUS) if line.length() > (_RADIUS*1.99): color = '#5a8bff' else: color = '#cc0000' nvhi_name = nvhi_name + '*' # mark as invalid line.setLength(_RADIUS) if neighbor_virtual_helix in wg_dict: wedge_item = wg_dict[neighbor_virtual_helix] else: wedge_item = WedgeGizmo(_RADIUS, WEDGE_RECT, self) wg_dict[neighbor_virtual_helix] = wedge_item wedge_item.showWedge(line.angle(), color, outline_only=False) self._added_wedge_gizmos.add(neighbor_virtual_helix) # end def def setNumber(self): """Updates the associated QGraphicsSimpleTextItem label text to match the id_num. Adjusts the label position so it is centered regardless of number of digits in the label. """ num = self.idNum() label = self._label if num is not None: label.setText("%d" % num) else: return y_val = _RADIUS / 3 if num < 10: label.setPos(_RADIUS / 1.5, y_val) elif num < 100: label.setPos(_RADIUS / 3, y_val) else: # _number >= 100 label.setPos(0, y_val) b_rect = label.boundingRect() posx = b_rect.width()/2 posy = b_rect.height()/2 label.setPos(_RADIUS-posx, _RADIUS-posy)
def __init__(self, model_virtual_helix, part_item): AbstractVirtualHelixItem.__init__(self, model_virtual_helix, part_item) CNOutlinerItem.__init__(self, model_virtual_helix, parent=part_item) self.setFlags(LEAF_FLAGS) self._controller = VirtualHelixItemController(self, model_virtual_helix.part(), False, False)
class PathVirtualHelixItem(AbstractVirtualHelixItem, QGraphicsPathItem): """VirtualHelixItem for PathView Attributes: drag_last_position (TYPE): Description FILTER_NAME (str): Description findChild (TYPE): Description handle_start (TYPE): Description is_active (bool): Description """ findChild = util.findChild # for debug FILTER_NAME = "virtual_helix" def __init__(self, model_virtual_helix, part_item, viewroot): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. part_item (TYPE): Description viewroot (TYPE): Description """ AbstractVirtualHelixItem.__init__(self, model_virtual_helix, part_item) QGraphicsPathItem.__init__(self, parent=part_item.proxy()) self._viewroot = viewroot self._getActiveTool = part_item._getActiveTool self._controller = VirtualHelixItemController(self, self._model_part, False, True) self._handle = VirtualHelixHandleItem(self, part_item, viewroot) self._last_strand_set = None self._last_idx = None self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption) self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.setBrush(getNoBrush()) view = self.view() view.levelOfDetailChangedSignal.connect(self.levelOfDetailChangedSlot) should_show_details = view.shouldShowDetails() pen = newPenObj(styles.MINOR_GRID_STROKE, styles.MINOR_GRID_STROKE_WIDTH) pen.setCosmetic(should_show_details) self.setPen(pen) self.is_active = False self.refreshPath() self.setAcceptHoverEvents(True) # for pathtools self.setZValue(styles.ZPATHHELIX) self._right_mouse_move = False self.drag_last_position = self.handle_start = self.pos() # end def ### SIGNALS ### ### SLOTS indirectly called from the part ### def levelOfDetailChangedSlot(self, boolval): """Not connected to the model, only the QGraphicsView Args: boolval (TYPE): Description """ pen = self.pen() pen.setCosmetic(boolval) # print("levelOfDetailChangedSlot", boolval, pen.width()) # if boolval: # pass # else: # pass self.setPen(pen) # end def def strandAddedSlot(self, sender, strand): """ Instantiates a StrandItem upon notification that the model has a new Strand. The StrandItem is responsible for creating its own controller for communication with the model, and for adding itself to its parent (which is *this* VirtualHelixItem, i.e. 'self'). Args: sender (obj): Model object that emitted the signal. strand (TYPE): Description """ StrandItem(strand, self, self._viewroot) # end def def virtualHelixRemovedSlot(self): """Summary Returns: TYPE: Description """ self.view().levelOfDetailChangedSignal.disconnect( self.levelOfDetailChangedSlot) self._controller.disconnectSignals() self._controller = None scene = self.scene() self._handle.remove() scene.removeItem(self) self._part_item = None self._model_part = None self._getActiveTool = None self._handle = None # end def def virtualHelixPropertyChangedSlot(self, keys, values): """Summary Args: keys (TYPE): Description values (TYPE): Description Returns: TYPE: Description """ for key, val in zip(keys, values): if key == 'is_visible': if val: self.show() self._handle.show() self.showXoverItems() else: self.hideXoverItems() self.hide() self._handle.hide() return if key == 'z': part_item = self._part_item z = part_item.convertToQtZ(val) if self.x() != z: self.setX(z) """ The handle is selected, so deselect to accurately position then reselect """ vhi_h = self._handle vhi_h.tempReparent() vhi_h.setX(z - _VH_XOFFSET) # if self.isSelected(): # print("ImZ", self.idNum()) part_item.updateXoverItems(self) vhi_h_selection_group = self._viewroot.vhiHandleSelectionGroup( ) vhi_h_selection_group.addToGroup(vhi_h) elif key == 'eulerZ': self._handle.rotateWithCenterOrigin(val) # self._prexoveritemgroup.updatePositionsAfterRotation(value) ### GEOMETRY PROPERTIES ### elif key == 'repeat_hint': pass # self.updateRepeats(int(val)) elif key == 'bases_per_repeat': pass # self.updateBasesPerRepeat(int(val)) elif key == 'turns_per_repeat': # self.updateTurnsPerRepeat(int(val)) pass ### RUNTIME PROPERTIES ### elif key == 'neighbors': # this means a virtual helix in the slice view has moved # so we need to clear and redraw the PreXoverItems just in case if self.isActive(): self._part_item.setPreXoverItemsVisible(self) self.refreshPath() # end def def showXoverItems(self): """Summary Returns: TYPE: Description """ for item in self.childItems(): if isinstance(item, XoverNode3): item.showXover() # end def def hideXoverItems(self): """Summary Returns: TYPE: Description """ for item in self.childItems(): if isinstance(item, XoverNode3): item.hideXover() # end def ### ACCESSORS ### def viewroot(self): """Summary Returns: TYPE: Description """ return self._viewroot # end def def handle(self): """Summary Returns: TYPE: Description """ return self._handle # end def def window(self): """Summary Returns: TYPE: Description """ return self._part_item.window() # end def def view(self): return self._viewroot.scene().views()[0] # end def ### DRAWING METHODS ### def upperLeftCornerOfBase(self, idx, strand): """Summary Args: idx (int): the base index within the virtual helix strand (TYPE): Description Returns: TYPE: Description """ x = idx * _BASE_WIDTH y = 0 if strand.isForward() else _BASE_WIDTH return x, y # end def def upperLeftCornerOfBaseType(self, idx, is_fwd): """Summary Args: idx (int): the base index within the virtual helix is_fwd (TYPE): Description Returns: TYPE: Description """ x = idx * _BASE_WIDTH y = 0 if is_fwd else _BASE_WIDTH return x, y # end def def refreshPath(self): """ Returns a QPainterPath object for the minor grid lines. The path also includes a border outline and a midline for dividing scaffold and staple bases. """ bw = _BASE_WIDTH bw2 = 2 * bw part = self.part() path = QPainterPath() sub_step_size = part.subStepSize() canvas_size = self._model_vh.getSize() # border path.addRect(0, 0, bw * canvas_size, 2 * bw) # minor tick marks for i in range(canvas_size): x = round(bw * i) + .5 if i % sub_step_size == 0: path.moveTo(x - .5, 0) path.lineTo(x - .5, bw2) path.lineTo(x - .25, bw2) path.lineTo(x - .25, 0) path.lineTo(x, 0) path.lineTo(x, bw2) path.lineTo(x + .25, bw2) path.lineTo(x + .25, 0) path.lineTo(x + .5, 0) path.lineTo(x + .5, bw2) # path.moveTo(x-.5, 0) # path.lineTo(x-.5, 2 * bw) # path.lineTo(x+.5, 2 * bw) # path.lineTo(x+.5, 0) else: path.moveTo(x, 0) path.lineTo(x, 2 * bw) # staple-scaffold divider path.moveTo(0, bw) path.lineTo(bw * canvas_size, bw) self.setPath(path) # end def def resize(self): """Called by part on resize. """ self.refreshPath() # end def ### PUBLIC SUPPORT METHODS ### def activate(self): """Summary Returns: TYPE: Description """ pen = self.pen() pen.setColor(getColorObj(styles.MINOR_GRID_STROKE_ACTIVE)) self.setPen(pen) self.is_active = True # end def def deactivate(self): """Summary Returns: TYPE: Description """ pen = self.pen() pen.setColor(getColorObj(styles.MINOR_GRID_STROKE)) self.setPen(pen) self.is_active = False # end def ### EVENT HANDLERS ### def mousePressEvent(self, event): """ Parses a mousePressEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ # 1. Check if we are doing a Z translation if event.button() == Qt.RightButton: viewroot = self._viewroot current_filter_set = viewroot.selectionFilterSet() if self.FILTER_NAME in current_filter_set and self.part( ).isZEditable(): self._right_mouse_move = True self.drag_last_position = event.scenePos() self.handle_start = self.pos() return self.scene().views()[0].addToPressList(self) strand_set, idx = self.baseAtPoint(event.pos()) self._model_vh.setActive(strand_set.isForward(), idx) tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "MousePress" if hasattr(self, tool_method_name): self._last_strand_set, self._last_idx = strand_set, idx getattr(self, tool_method_name)(strand_set, idx, event.modifiers()) else: event.setAccepted(False) # end def def mouseMoveEvent(self, event): """ Parses a mouseMoveEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ # 1. Check if we are doing a Z translation if self._right_mouse_move: MOVE_THRESHOLD = 0.01 # ignore small moves new_pos = event.scenePos() delta = new_pos - self.drag_last_position dx = int(floor(delta.x() / _BASE_WIDTH)) * _BASE_WIDTH x = self.handle_start.x() + dx if abs(dx) > MOVE_THRESHOLD or dx == 0.0: old_x = self.x() self.setX(x) vhi_h = self._handle vhi_h.tempReparent() vhi_h.setX(x - _VH_XOFFSET) self._part_item.updateXoverItems(self) dz = self._part_item.convertToModelZ(x - old_x) self._model_part.translateVirtualHelices([self.idNum()], 0, 0, dz, False, use_undostack=False) return # 2. Forward event to tool tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "MouseMove" if hasattr(self, tool_method_name): strand_set, idx = self.baseAtPoint(event.pos()) if self._last_strand_set != strand_set or self._last_idx != idx: self._last_strand_set, self._last_idx = strand_set, idx getattr(self, tool_method_name)(strand_set, idx) else: event.setAccepted(False) # end def def mouseReleaseEvent(self, event): """Called in the event of doing a Z translation drag Args: event (TYPE): Description """ if self._right_mouse_move and event.button() == Qt.RightButton: MOVE_THRESHOLD = 0.01 # ignore small moves self._right_mouse_move = False delta = self.pos() - self.handle_start dz = delta.x() if abs(dz) > MOVE_THRESHOLD: dz = self._part_item.convertToModelZ(dz) self._model_part.translateVirtualHelices([self.idNum()], 0, 0, dz, True, use_undostack=True) # end def def customMouseRelease(self, event): """ Parses a mouseReleaseEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "MouseRelease" if hasattr(self, tool_method_name): getattr(self, tool_method_name)(self._last_strand_set, self._last_idx) else: event.setAccepted(False) # end def ### COORDINATE UTILITIES ### def baseAtPoint(self, pos): """ Returns the (Strandset, index) under the location x, y or None. It shouldn't be possible to click outside a pathhelix and still call this function. However, this sometimes happens if you click exactly on the top or bottom edge, resulting in a negative y value. Args: pos (TYPE): Description """ x, y = pos.x(), pos.y() part = self._model_part id_num = self._id_num base_idx = int(floor(x / _BASE_WIDTH)) min_base, max_base = 0, part.maxBaseIdx(id_num) if base_idx < min_base or base_idx >= max_base: base_idx = util.clamp(base_idx, min_base, max_base) if y < 0: y = 0 # HACK: zero out y due to erroneous click strand_type = floor(y * 1. / _BASE_WIDTH) # 0 for fwd, 1 for rev strand_type = int(util.clamp(strand_type, 0, 1)) strand_set = part.getStrandSets(id_num)[strand_type] return (strand_set, base_idx) # end def def keyPanDeltaX(self): """How far a single press of the left or right arrow key should move the scene (in scene space) """ dx = self._part_item.part().stepSize() * _BASE_WIDTH return self.mapToScene(QRectF(0, 0, dx, 1)).boundingRect().width() # end def def hoverLeaveEvent(self, event): """Summary Args: event (TYPE): Description Returns: TYPE: Description """ self._part_item.updateStatusBar("") # end def def hoverMoveEvent(self, event): """ Parses a mouseMoveEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ base_idx = int(floor(event.pos().x() / _BASE_WIDTH)) loc = "%d[%d]" % (self._id_num, base_idx) self._part_item.updateStatusBar(loc) active_tool = self._getActiveTool() tool_method_name = active_tool.methodPrefix() + "HoverMove" if hasattr(self, tool_method_name): is_fwd, idx_x, idx_y = active_tool.baseAtPoint(self, event.pos()) getattr(self, tool_method_name)(is_fwd, idx_x, idx_y) # end def ### TOOL METHODS ### def pencilToolMousePress(self, strand_set, idx, modifiers): """strand.getDragBounds Args: strand_set (StrandSet): Description idx (int): the base index within the virtual helix """ # print("%s: %s[%s]" % (util.methodName(), strand_set, idx)) if modifiers & Qt.ShiftModifier: bounds = strand_set.getBoundsOfEmptyRegionContaining(idx) ret = strand_set.createStrand(*bounds) print("creating strand {} was successful: {}".format(bounds, ret)) return active_tool = self._getActiveTool() if not active_tool.isDrawingStrand(): active_tool.initStrandItemFromVHI(self, strand_set, idx) active_tool.setIsDrawingStrand(True) # end def def pencilToolMouseMove(self, strand_set, idx): """strand.getDragBounds Args: strand_set (StrandSet): Description idx (int): the base index within the virtual helix """ # print("%s: %s[%s]" % (util.methodName(), strand_set, idx)) active_tool = self._getActiveTool() if active_tool.isDrawingStrand(): active_tool.updateStrandItemFromVHI(self, strand_set, idx) # end def def pencilToolMouseRelease(self, strand_set, idx): """strand.getDragBounds Args: strand_set (StrandSet): Description idx (int): the base index within the virtual helix """ # print("%s: %s[%s]" % (util.methodName(), strand_set, idx)) active_tool = self._getActiveTool() if active_tool.isDrawingStrand(): active_tool.setIsDrawingStrand(False) active_tool.attemptToCreateStrand(self, strand_set, idx) # end def def pencilToolHoverMove(self, is_fwd, idx_x, idx_y): """Pencil the strand is possible. Args: is_fwd (TYPE): Description idx_x (TYPE): Description idx_y (TYPE): Description """ active_tool = self._getActiveTool() if not active_tool.isFloatingXoverBegin(): temp_xover = active_tool.floatingXover() temp_xover.updateFloatingFromVHI(self, is_fwd, idx_x, idx_y)
class VirtualHelixItem(QGraphicsEllipseItem): """ The VirtualHelixItem is an individual circle that gets drawn in the SliceView as a child of the PartItem. Taken as a group, many SliceHelix instances make up the crossection of the DNAPart. Clicking on a SliceHelix adds a VirtualHelix to the DNAPart. The SliceHelix then changes appearence and paints its corresponding VirtualHelix number. """ # set up default, hover, and active drawing styles _USE_BRUSH = QBrush(styles.ORANGE_FILL) _USE_PEN = QPen(styles.ORANGE_STROKE, styles.SLICE_HELIX_STROKE_WIDTH) _RADIUS = styles.SLICE_HELIX_RADIUS _OUT_OF_SLICE_PEN = QPen(styles.LIGHT_ORANGE_STROKE,\ styles.SLICE_HELIX_STROKE_WIDTH) _OUT_OF_SLICE_BRUSH = QBrush(styles.LIGHT_ORANGE_FILL) _RECT = QRectF(0, 0, 2 * _RADIUS, 2 * _RADIUS) _FONT = styles.SLICE_NUM_FONT _ZVALUE = styles.ZSLICEHELIX+3 def __init__(self, model_virtual_helix, empty_helix_item): """ empty_helix_item is a EmptyHelixItem that will act as a QGraphicsItem parent """ super(VirtualHelixItem, self).__init__(parent=empty_helix_item) self._virtual_helix = model_virtual_helix self._empty_helix_item = empty_helix_item self.hide() # drawing related self.isHovered = False self.setAcceptHoverEvents(True) # self.setFlag(QGraphicsItem.ItemIsSelectable) self.setZValue(self._ZVALUE) self.lastMousePressAddedBases = False self.setBrush(self._OUT_OF_SLICE_BRUSH) self.setPen(self._OUT_OF_SLICE_PEN) self.setRect(self._RECT) # handle the label specific stuff self._label = self.createLabel() self.setNumber() self._pen1, self._pen2 = (QPen(), QPen()) self.createArrows() self._controller = VirtualHelixItemController(self, model_virtual_helix) self.show() # end def ### SIGNALS ### ### SLOTS ### def virtualHelixNumberChangedSlot(self, virtualHelix): """ receives a signal containing a virtualHelix and the oldNumber as a safety check """ self.setNumber() # end def def virtualHelixRemovedSlot(self, virtualHelix): self._controller.disconnectSignals() self._controller = None self._empty_helix_item.setNotHovered() self._virtual_helix = None self._empty_helix_item = None self.scene().removeItem(self._label) self._label = None self.scene().removeItem(self) # end def def strandAddedSlot(self, sender, strand): pass # end def def createLabel(self): label = QGraphicsSimpleTextItem("%d" % self._virtual_helix.number()) label.setFont(self._FONT) label.setZValue(self._ZVALUE) label.setParentItem(self) return label # end def def createArrows(self): rad = self._RADIUS pen1 = self._pen1 pen2 = self._pen2 pen1.setWidth(3) pen2.setWidth(3) pen1.setBrush(Qt.gray) pen2.setBrush(Qt.lightGray) if self._virtual_helix.isEvenParity(): arrow1 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow2 = QGraphicsLineItem(0, rad, rad, rad, self) else: arrow1 = QGraphicsLineItem(0, rad, rad, rad, self) arrow2 = QGraphicsLineItem(rad, rad, 2*rad, rad, self) arrow1.setTransformOriginPoint(rad, rad) arrow2.setTransformOriginPoint(rad, rad) arrow1.setZValue(400) arrow2.setZValue(400) arrow1.setPen(pen1) arrow2.setPen(pen2) self.arrow1 = arrow1 self.arrow2 = arrow2 self.arrow1.hide() self.arrow2.hide() # end def def updateScafArrow(self, idx): scafStrand = self._virtual_helix.scaf(idx) if scafStrand: scafStrandColor = QColor(scafStrand.oligo().color()) scafAlpha = 0.9 if scafStrand.hasXoverAt(idx) else 0.3 else: scafStrandColor = QColor(Qt.gray) scafAlpha = 0.1 scafStrandColor.setAlphaF(scafAlpha) self._pen1.setBrush(scafStrandColor) self.arrow1.setPen(self._pen1) part = self.part() tpb = part._TWIST_PER_BASE angle = idx*tpb # for some reason rotation is CW and not CCW with increasing angle self.arrow1.setRotation(angle + part._TWIST_OFFSET) def updateStapArrow(self, idx): stapStrand = self._virtual_helix.stap(idx) if stapStrand: stapStrandColor = QColor(stapStrand.oligo().color()) stapAlpha = 0.9 if stapStrand.hasXoverAt(idx) else 0.3 else: stapStrandColor = QColor(Qt.lightGray) stapAlpha = 0.1 stapStrandColor.setAlphaF(stapAlpha) self._pen2.setBrush(stapStrandColor) self.arrow2.setPen(self._pen2) part = self.part() tpb = part._TWIST_PER_BASE angle = idx*tpb self.arrow2.setRotation(angle + part._TWIST_OFFSET) # end def def setNumber(self): """docstring for setNumber""" vh = self._virtual_helix num = vh.number() label = self._label radius = self._RADIUS if num != None: label.setText("%d" % num) else: return y_val = radius / 3 if num < 10: label.setPos(radius / 1.5, y_val) elif num < 100: label.setPos(radius / 3, y_val) else: # _number >= 100 label.setPos(0, y_val) b_rect = label.boundingRect() posx = b_rect.width()/2 posy = b_rect.height()/2 label.setPos(radius-posx, radius-posy) # end def def part(self): return self._empty_helix_item.part() def virtualHelix(self): return self._virtual_helix # end def def number(self): return self.virtualHelix().number() def setActiveSliceView(self, idx, has_scaf, has_stap): if has_scaf: self.setPen(self._USE_PEN) self.setBrush(self._USE_BRUSH) self.updateScafArrow(idx) self.arrow1.show() else: self.setPen(self._OUT_OF_SLICE_PEN) self.setBrush(self._OUT_OF_SLICE_BRUSH) self.arrow1.hide() if has_stap: self.updateStapArrow(idx) self.arrow2.show() else: self.arrow2.hide() # end def ############################ User Interaction ############################ def sceneEvent(self, event): """Included for unit testing in order to grab events that are sent via QGraphicsScene.sendEvent().""" # if self._parent.sliceController.testRecorder: # coord = (self._row, self._col) # self._parent.sliceController.testRecorder.sliceSceneEvent(event, coord) if event.type() == QEvent.MouseButtonPress: self.mousePressEvent(event) return True elif event.type() == QEvent.MouseButtonRelease: self.mouseReleaseEvent(event) return True elif event.type() == QEvent.MouseMove: self.mouseMoveEvent(event) return True QGraphicsItem.sceneEvent(self, event) return False def hoverEnterEvent(self, event): """ If the selection is configured to always select everything, we don't draw a focus ring around everything, instead we only draw a focus ring around the hovered obj. """ # if self.selectAllBehavior(): # self.setSelected(True) # forward the event to the empty_helix_item as well self._empty_helix_item.hoverEnterEvent(event) # end def def hoverLeaveEvent(self, event): # if self.selectAllBehavior(): # self.setSelected(False) self._empty_helix_item.hoverEnterEvent(event)
class PathVirtualHelixItem(AbstractVirtualHelixItem, QGraphicsPathItem): """VirtualHelixItem for PathView Attributes: drag_last_position (TYPE): Description FILTER_NAME (str): Description findChild (TYPE): Description handle_start (TYPE): Description is_active (bool): Description """ findChild = util.findChild # for debug FILTER_NAME = "virtual_helix" def __init__(self, model_virtual_helix, part_item, viewroot): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. part_item (TYPE): Description viewroot (TYPE): Description """ AbstractVirtualHelixItem.__init__(self, model_virtual_helix, part_item) QGraphicsPathItem.__init__(self, parent=part_item.proxy()) self._viewroot = viewroot self._getActiveTool = part_item._getActiveTool self._controller = VirtualHelixItemController(self, self._model_part, False, True) self._handle = VirtualHelixHandleItem(self, part_item, viewroot) self._last_strand_set = None self._last_idx = None self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption) self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.setBrush(getNoBrush()) view = self.view() view.levelOfDetailChangedSignal.connect(self.levelOfDetailChangedSlot) should_show_details = view.shouldShowDetails() pen = newPenObj(styles.MINOR_GRID_STROKE, styles.MINOR_GRID_STROKE_WIDTH) pen.setCosmetic(should_show_details) self.setPen(pen) self.is_active = False self.refreshPath() self.setAcceptHoverEvents(True) # for pathtools self.setZValue(styles.ZPATHHELIX) self._right_mouse_move = False self.drag_last_position = self.handle_start = self.pos() # end def ### SIGNALS ### ### SLOTS indirectly called from the part ### def levelOfDetailChangedSlot(self, boolval): """Not connected to the model, only the QGraphicsView Args: boolval (TYPE): Description """ pen = self.pen() pen.setCosmetic(boolval) # print("levelOfDetailChangedSlot", boolval, pen.width()) # if boolval: # pass # else: # pass self.setPen(pen) # end def def strandAddedSlot(self, sender, strand): """ Instantiates a StrandItem upon notification that the model has a new Strand. The StrandItem is responsible for creating its own controller for communication with the model, and for adding itself to its parent (which is *this* VirtualHelixItem, i.e. 'self'). Args: sender (obj): Model object that emitted the signal. strand (TYPE): Description """ StrandItem(strand, self, self._viewroot) # end def def virtualHelixRemovedSlot(self): """Summary Returns: TYPE: Description """ self.view().levelOfDetailChangedSignal.disconnect(self.levelOfDetailChangedSlot) self._controller.disconnectSignals() self._controller = None scene = self.scene() self._handle.remove() scene.removeItem(self) self._part_item = None self._model_part = None self._getActiveTool = None self._handle = None # end def def virtualHelixPropertyChangedSlot(self, keys, values): """Summary Args: keys (TYPE): Description values (TYPE): Description Returns: TYPE: Description """ for key, val in zip(keys, values): if key == 'is_visible': if val: self.show() self._handle.show() self.showXoverItems() else: self.hideXoverItems() self.hide() self._handle.hide() return if key == 'z': part_item = self._part_item z = part_item.convertToQtZ(val) if self.x() != z: self.setX(z) """ The handle is selected, so deselect to accurately position then reselect """ vhi_h = self._handle vhi_h.tempReparent() vhi_h.setX(z - _VH_XOFFSET) # if self.isSelected(): # print("ImZ", self.idNum()) part_item.updateXoverItems(self) vhi_h_selection_group = self._viewroot.vhiHandleSelectionGroup() vhi_h_selection_group.addToGroup(vhi_h) elif key == 'eulerZ': self._handle.rotateWithCenterOrigin(val) # self._prexoveritemgroup.updatePositionsAfterRotation(value) ### GEOMETRY PROPERTIES ### elif key == 'repeat_hint': pass # self.updateRepeats(int(val)) elif key == 'bases_per_repeat': pass # self.updateBasesPerRepeat(int(val)) elif key == 'turns_per_repeat': # self.updateTurnsPerRepeat(int(val)) pass ### RUNTIME PROPERTIES ### elif key == 'neighbors': # this means a virtual helix in the slice view has moved # so we need to clear and redraw the PreXoverItems just in case if self.isActive(): self._part_item.setPreXoverItemsVisible(self) self.refreshPath() # end def def showXoverItems(self): """Summary Returns: TYPE: Description """ for item in self.childItems(): if isinstance(item, XoverNode3): item.showXover() # end def def hideXoverItems(self): """Summary Returns: TYPE: Description """ for item in self.childItems(): if isinstance(item, XoverNode3): item.hideXover() # end def ### ACCESSORS ### def viewroot(self): """Summary Returns: TYPE: Description """ return self._viewroot # end def def handle(self): """Summary Returns: TYPE: Description """ return self._handle # end def def window(self): """Summary Returns: TYPE: Description """ return self._part_item.window() # end def def view(self): return self._viewroot.scene().views()[0] # end def ### DRAWING METHODS ### def upperLeftCornerOfBase(self, idx, strand): """Summary Args: idx (int): the base index within the virtual helix strand (TYPE): Description Returns: TYPE: Description """ x = idx * _BASE_WIDTH y = 0 if strand.isForward() else _BASE_WIDTH return x, y # end def def upperLeftCornerOfBaseType(self, idx, is_fwd): """Summary Args: idx (int): the base index within the virtual helix is_fwd (TYPE): Description Returns: TYPE: Description """ x = idx * _BASE_WIDTH y = 0 if is_fwd else _BASE_WIDTH return x, y # end def def refreshPath(self): """ Returns a QPainterPath object for the minor grid lines. The path also includes a border outline and a midline for dividing scaffold and staple bases. """ bw = _BASE_WIDTH bw2 = 2 * bw part = self.part() path = QPainterPath() sub_step_size = part.subStepSize() canvas_size = self._model_vh.getSize() # border path.addRect(0, 0, bw * canvas_size, 2 * bw) # minor tick marks for i in range(canvas_size): x = round(bw * i) + .5 if i % sub_step_size == 0: path.moveTo(x - .5, 0) path.lineTo(x - .5, bw2) path.lineTo(x - .25, bw2) path.lineTo(x - .25, 0) path.lineTo(x, 0) path.lineTo(x, bw2) path.lineTo(x + .25, bw2) path.lineTo(x + .25, 0) path.lineTo(x + .5, 0) path.lineTo(x + .5, bw2) # path.moveTo(x-.5, 0) # path.lineTo(x-.5, 2 * bw) # path.lineTo(x+.5, 2 * bw) # path.lineTo(x+.5, 0) else: path.moveTo(x, 0) path.lineTo(x, 2 * bw) # staple-scaffold divider path.moveTo(0, bw) path.lineTo(bw * canvas_size, bw) self.setPath(path) # end def def resize(self): """Called by part on resize. """ self.refreshPath() # end def ### PUBLIC SUPPORT METHODS ### def activate(self): """Summary Returns: TYPE: Description """ pen = self.pen() pen.setColor(getColorObj(styles.MINOR_GRID_STROKE_ACTIVE)) self.setPen(pen) self.is_active = True # end def def deactivate(self): """Summary Returns: TYPE: Description """ pen = self.pen() pen.setColor(getColorObj(styles.MINOR_GRID_STROKE)) self.setPen(pen) self.is_active = False # end def ### EVENT HANDLERS ### def mousePressEvent(self, event): """ Parses a mousePressEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ # 1. Check if we are doing a Z translation if event.button() == Qt.RightButton: viewroot = self._viewroot current_filter_set = viewroot.selectionFilterSet() if self.FILTER_NAME in current_filter_set and self.part().isZEditable(): self._right_mouse_move = True self.drag_last_position = event.scenePos() self.handle_start = self.pos() return self.scene().views()[0].addToPressList(self) strand_set, idx = self.baseAtPoint(event.pos()) self._model_vh.setActive(strand_set.isForward(), idx) tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "MousePress" if hasattr(self, tool_method_name): self._last_strand_set, self._last_idx = strand_set, idx getattr(self, tool_method_name)(strand_set, idx, event.modifiers()) else: event.setAccepted(False) # end def def mouseMoveEvent(self, event): """ Parses a mouseMoveEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ # 1. Check if we are doing a Z translation if self._right_mouse_move: MOVE_THRESHOLD = 0.01 # ignore small moves new_pos = event.scenePos() delta = new_pos - self.drag_last_position dx = int(floor(delta.x() / _BASE_WIDTH))*_BASE_WIDTH x = self.handle_start.x() + dx if abs(dx) > MOVE_THRESHOLD or dx == 0.0: old_x = self.x() self.setX(x) vhi_h = self._handle vhi_h.tempReparent() vhi_h.setX(x - _VH_XOFFSET) self._part_item.updateXoverItems(self) dz = self._part_item.convertToModelZ(x - old_x) self._model_part.translateVirtualHelices([self.idNum()], 0, 0, dz, False, use_undostack=False) return # 2. Forward event to tool tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "MouseMove" if hasattr(self, tool_method_name): strand_set, idx = self.baseAtPoint(event.pos()) if self._last_strand_set != strand_set or self._last_idx != idx: self._last_strand_set, self._last_idx = strand_set, idx getattr(self, tool_method_name)(strand_set, idx) else: event.setAccepted(False) # end def def mouseReleaseEvent(self, event): """Called in the event of doing a Z translation drag Args: event (TYPE): Description """ if self._right_mouse_move and event.button() == Qt.RightButton: MOVE_THRESHOLD = 0.01 # ignore small moves self._right_mouse_move = False delta = self.pos() - self.handle_start dz = delta.x() if abs(dz) > MOVE_THRESHOLD: dz = self._part_item.convertToModelZ(dz) self._model_part.translateVirtualHelices([self.idNum()], 0, 0, dz, True, use_undostack=True) # end def def customMouseRelease(self, event): """ Parses a mouseReleaseEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "MouseRelease" if hasattr(self, tool_method_name): getattr(self, tool_method_name)(self._last_strand_set, self._last_idx) else: event.setAccepted(False) # end def ### COORDINATE UTILITIES ### def baseAtPoint(self, pos): """ Returns the (Strandset, index) under the location x, y or None. It shouldn't be possible to click outside a pathhelix and still call this function. However, this sometimes happens if you click exactly on the top or bottom edge, resulting in a negative y value. Args: pos (TYPE): Description """ x, y = pos.x(), pos.y() part = self._model_part id_num = self._id_num base_idx = int(floor(x / _BASE_WIDTH)) min_base, max_base = 0, part.maxBaseIdx(id_num) if base_idx < min_base or base_idx >= max_base: base_idx = util.clamp(base_idx, min_base, max_base) if y < 0: y = 0 # HACK: zero out y due to erroneous click strand_type = floor(y * 1. / _BASE_WIDTH) # 0 for fwd, 1 for rev strand_type = int(util.clamp(strand_type, 0, 1)) strand_set = part.getStrandSets(id_num)[strand_type] return (strand_set, base_idx) # end def def keyPanDeltaX(self): """How far a single press of the left or right arrow key should move the scene (in scene space) """ dx = self._part_item.part().stepSize() * _BASE_WIDTH return self.mapToScene(QRectF(0, 0, dx, 1)).boundingRect().width() # end def def hoverLeaveEvent(self, event): """Summary Args: event (TYPE): Description Returns: TYPE: Description """ self._part_item.updateStatusBar("") # end def def hoverMoveEvent(self, event): """ Parses a mouseMoveEvent to extract strand_set and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ base_idx = int(floor(event.pos().x() / _BASE_WIDTH)) loc = "%d[%d]" % (self._id_num, base_idx) self._part_item.updateStatusBar(loc) active_tool = self._getActiveTool() tool_method_name = active_tool.methodPrefix() + "HoverMove" if hasattr(self, tool_method_name): is_fwd, idx_x, idx_y = active_tool.baseAtPoint(self, event.pos()) getattr(self, tool_method_name)(is_fwd, idx_x, idx_y) # end def ### TOOL METHODS ### def pencilToolMousePress(self, strand_set, idx, modifiers): """strand.getDragBounds Args: strand_set (StrandSet): Description idx (int): the base index within the virtual helix """ # print("%s: %s[%s]" % (util.methodName(), strand_set, idx)) if modifiers & Qt.ShiftModifier: bounds = strand_set.getBoundsOfEmptyRegionContaining(idx) ret = strand_set.createStrand(*bounds) print("creating strand {} was successful: {}".format(bounds, ret)) return active_tool = self._getActiveTool() if not active_tool.isDrawingStrand(): active_tool.initStrandItemFromVHI(self, strand_set, idx) active_tool.setIsDrawingStrand(True) # end def def pencilToolMouseMove(self, strand_set, idx): """strand.getDragBounds Args: strand_set (StrandSet): Description idx (int): the base index within the virtual helix """ # print("%s: %s[%s]" % (util.methodName(), strand_set, idx)) active_tool = self._getActiveTool() if active_tool.isDrawingStrand(): active_tool.updateStrandItemFromVHI(self, strand_set, idx) # end def def pencilToolMouseRelease(self, strand_set, idx): """strand.getDragBounds Args: strand_set (StrandSet): Description idx (int): the base index within the virtual helix """ # print("%s: %s[%s]" % (util.methodName(), strand_set, idx)) active_tool = self._getActiveTool() if active_tool.isDrawingStrand(): active_tool.setIsDrawingStrand(False) active_tool.attemptToCreateStrand(self, strand_set, idx) # end def def pencilToolHoverMove(self, is_fwd, idx_x, idx_y): """Pencil the strand is possible. Args: is_fwd (TYPE): Description idx_x (TYPE): Description idx_y (TYPE): Description """ active_tool = self._getActiveTool() if not active_tool.isFloatingXoverBegin(): temp_xover = active_tool.floatingXover() temp_xover.updateFloatingFromVHI(self, is_fwd, idx_x, idx_y)