class SliceNucleicAcidPartItem(QAbstractPartItem): """Parent should be either a SliceRootItem, or an AssemblyItem. Invariant: keys in _empty_helix_hash = range(_nrows) x range(_ncols) where x is the cartesian product. Attributes: active_virtual_helix_item (cadnano.views.sliceview.virtualhelixitem.SliceVirtualHelixItem): Description resize_handle_group (ResizeHandleGroup): handles for dragging and resizing griditem (GridItem): Description outline (QGraphicsRectItem): Description prexover_manager (PreXoverManager): Description scale_factor (float): Description """ _RADIUS = styles.SLICE_HELIX_RADIUS _BOUNDING_RECT_PADDING = 80 def __init__(self, model_part_instance, viewroot, parent=None): """Summary Args: model_part_instance (TYPE): Description viewroot (TYPE): Description parent (None, optional): Description """ super(SliceNucleicAcidPartItem, self).__init__(model_part_instance, viewroot, parent) self.shortest_path_start = None self.neighbor_map = dict() self.coordinates_to_vhid = dict() self.coordinates_to_xy = dict() self._last_hovered_item = None self._highlighted_path = [] self.spa_start_vhi = None self.last_mouse_position = None self._translated_x = 0.0 self._translated_y = 0.0 self._getActiveTool = viewroot.manager.activeToolGetter m_p = self._model_part self._controller = NucleicAcidPartItemController(self, m_p) self.scale_factor = self._RADIUS / m_p.radius() self.inverse_scale_factor = m_p.radius() / self._RADIUS self.active_virtual_helix_item = None self.prexover_manager = PreXoverManager(self) self.hide() # hide while until after attemptResize() to avoid flicker self._rect = QRectF(0., 0., 1000., 1000.) # set this to a token value self.boundRectToModel() self.setPen(getNoPen()) self.setRect(self._rect) self.setAcceptHoverEvents(True) self.shortest_path_add_mode = False # Cache of VHs that were active as of last call to activeSliceChanged # If None, all slices will be redrawn and the cache will be filled. # Connect destructor. This is for removing a part from scenes. # initialize the NucleicAcidPartItem with an empty set of old coords self.setZValue(styles.ZPARTITEM) self.outline = outline = QGraphicsRectItem(self) o_rect = self._configureOutline(outline) outline.setFlag(QGraphicsItem.ItemStacksBehindParent) outline.setZValue(styles.ZDESELECTOR) model_color = m_p.getColor() self.outline.setPen(getPenObj(model_color, _DEFAULT_WIDTH)) self.model_bounds_hint = QGraphicsRectItem(self) self.model_bounds_hint.setBrush(getBrushObj(model_color, alpha=12)) self.model_bounds_hint.setPen(getNoPen()) self.resize_handle_group = ResizeHandleGroup( o_rect, _HANDLE_SIZE, model_color, True, HandleType.TOP | HandleType.BOTTOM | HandleType.LEFT | HandleType.RIGHT | HandleType.TOP_LEFT | HandleType.TOP_RIGHT | HandleType.BOTTOM_LEFT | HandleType.BOTTOM_RIGHT, self, show_coords=True) self.griditem = GridItem(self, self._model_props['grid_type']) self.griditem.setZValue(1) self.resize_handle_group.setZValue(2) self.x_axis_line = QGraphicsLineItem(0, 0, self._RADIUS, 0, self) self.x_axis_line.setPen(getPenObj('#cc0000', _DEFAULT_WIDTH)) self.x_axis_line.setZValue(styles.ZAXIS) self.y_axis_line = QGraphicsLineItem(0, 0, 0, -self._RADIUS, self) self.y_axis_line.setPen(getPenObj('#007200', _DEFAULT_WIDTH)) self.y_axis_line.setZValue(styles.ZAXIS) # select upon creation for part in m_p.document().children(): if part is m_p: part.setSelected(True) else: part.setSelected(False) self.show() # end def ### SIGNALS ### ### SLOTS ### def partActiveVirtualHelixChangedSlot(self, part, id_num): """Summary Args: part (NucleicAcidPart): Description id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. """ vhi = self._virtual_helix_item_hash.get(id_num) self.setActiveVirtualHelixItem(vhi) self.setPreXoverItemsVisible(vhi) # end def def partActiveBaseInfoSlot(self, part, info): """Summary Args: part (TYPE): Description info (TYPE): Description Args: TYPE: Description """ pxom = self.prexover_manager pxom.deactivateNeighbors() if info and info is not None: id_num, is_fwd, idx, _ = info pxom.activateNeighbors(id_num, is_fwd, idx) # end def def partPropertyChangedSlot(self, model_part, property_key, new_value): """Summary Args: model_part (Part): The model part property_key (TYPE): Description new_value (TYPE): Description Args: TYPE: Description """ if self._model_part == model_part: self._model_props[property_key] = new_value if property_key == 'color': self.outline.setPen(getPenObj(new_value, _DEFAULT_WIDTH)) for vhi in self._virtual_helix_item_hash.values(): vhi.updateAppearance() self.resize_handle_group.setPens(getPenObj(new_value, 0)) elif property_key == 'is_visible': if new_value: self.show() else: self.hide() elif property_key == 'grid_type': self.griditem.setGridType(new_value) # end def def partRemovedSlot(self, sender): """docstring for partRemovedSlot Args: sender (obj): Model object that emitted the signal. """ self.parentItem().removePartItem(self) scene = self.scene() scene.removeItem(self) self._model_part = None self._mod_circ = None self._controller.disconnectSignals() self._controller = None self.resize_handle_group.removeHandles() self.griditem = None # end def def partVirtualHelicesTranslatedSlot(self, sender, vh_set, left_overs, do_deselect): """ left_overs are neighbors that need updating due to changes Args: sender (obj): Model object that emitted the signal. vh_set (TYPE): Description left_overs (TYPE): Description do_deselect (TYPE): Description """ if do_deselect: tool = self._getActiveTool() if tool.methodPrefix() == "selectTool": if tool.isSelectionActive(): # tool.deselectItems() tool.modelClear() # 1. move everything that moved for id_num in vh_set: vhi = self._virtual_helix_item_hash[id_num] vhi.updatePosition() # 2. now redraw what makes sense to be redrawn for id_num in vh_set: vhi = self._virtual_helix_item_hash[id_num] self._refreshVirtualHelixItemGizmos(id_num, vhi) for id_num in left_overs: vhi = self._virtual_helix_item_hash[id_num] self._refreshVirtualHelixItemGizmos(id_num, vhi) # 0. clear PreXovers: # self.prexover_manager.hideGroups() # if self.active_virtual_helix_item is not None: # self.active_virtual_helix_item.deactivate() # self.active_virtual_helix_item = None avhi = self.active_virtual_helix_item self.setPreXoverItemsVisible(avhi) self.enlargeRectToFit() # end def def _refreshVirtualHelixItemGizmos(self, id_num, vhi): """Update props and appearance of self & recent neighbors. Ultimately triggered by a partVirtualHelicesTranslatedSignal. Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. vhi (cadnano.views.sliceview.virtualhelixitem.SliceVirtualHelixItem): the item associated with id_num """ neighbors = vhi.cnModel().getProperty('neighbors') neighbors = literal_eval(neighbors) vhi.beginAddWedgeGizmos() for nvh in neighbors: nvhi = self._virtual_helix_item_hash.get(nvh, False) if nvhi: vhi.setWedgeGizmo(nvh, nvhi) # end for vhi.endAddWedgeGizmos() # end def def partVirtualHelixPropertyChangedSlot(self, sender, id_num, virtual_helix, keys, values): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. keys (tuple): keys that changed values (tuple): new values for each key that changed Args: TYPE: Description """ if self._model_part == sender: vh_i = self._virtual_helix_item_hash[id_num] vh_i.virtualHelixPropertyChangedSlot(keys, values) # end def def partVirtualHelixAddedSlot(self, sender, id_num, virtual_helix, neighbors): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. neighbors (TYPE): Description Args: TYPE: Description """ vhi = SliceVirtualHelixItem(virtual_helix, self) self._virtual_helix_item_hash[id_num] = vhi self._refreshVirtualHelixItemGizmos(id_num, vhi) for neighbor_id in neighbors: nvhi = self._virtual_helix_item_hash.get(neighbor_id, False) if nvhi: self._refreshVirtualHelixItemGizmos(neighbor_id, nvhi) self.enlargeRectToFit() position = sender.locationQt(id_num=id_num, scale_factor=self.scale_factor) coordinates = ShortestPathHelper.findClosestPoint( position=position, point_map=self.coordinates_to_xy) assert id_num not in self.coordinates_to_vhid.values() self.coordinates_to_vhid[coordinates] = id_num assert len(self.coordinates_to_vhid.keys()) == len( set(self.coordinates_to_vhid.keys())) assert len(self.coordinates_to_vhid.values()) == len( set(self.coordinates_to_vhid.values())) # end def def partVirtualHelixRemovingSlot(self, sender, id_num, virtual_helix, neighbors): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. neighbors (TYPE): Description Args: TYPE: Description """ tm = self._viewroot.manager tm.resetTools() self.removeVirtualHelixItem(id_num) for neighbor_id in neighbors: nvhi = self._virtual_helix_item_hash[neighbor_id] self._refreshVirtualHelixItemGizmos(neighbor_id, nvhi) for coordinates, current_id in self.coordinates_to_vhid.items(): if current_id == id_num: del self.coordinates_to_vhid[coordinates] break assert id_num not in self.coordinates_to_vhid.values() assert len(self.coordinates_to_vhid.keys()) == len( set(self.coordinates_to_vhid.keys())) assert len(self.coordinates_to_vhid.values()) == len( set(self.coordinates_to_vhid.values())) # end def def partSelectedChangedSlot(self, model_part, is_selected): """Set this Z to front, and return other Zs to default. Args: model_part (Part): The model part is_selected (TYPE): Description """ if is_selected: # self._drag_handle.resetAppearance(_SELECTED_COLOR, _SELECTED_WIDTH, _SELECTED_ALPHA) self.setZValue(styles.ZPARTITEM + 1) else: # self._drag_handle.resetAppearance(self.modelColor(), _DEFAULT_WIDTH, _DEFAULT_ALPHA) self.setZValue(styles.ZPARTITEM) # end def def partVirtualHelicesSelectedSlot(self, sender, vh_set, is_adding): """is_adding (bool): adding (True) virtual helices to a selection or removing (False) Args: sender (obj): Model object that emitted the signal. vh_set (TYPE): Description is_adding (TYPE): Description """ select_tool = self._viewroot.select_tool if is_adding: select_tool.selection_set.update(vh_set) select_tool.setPartItem(self) select_tool.getSelectionBoundingRect() else: select_tool.deselectSet(vh_set) # end def def partDocumentSettingChangedSlot(self, part, key, value): """Summary Args: part (TYPE): Description key (TYPE): Description value (TYPE): Description Args: TYPE: Description Raises: ValueError: Description """ if key == 'grid': if value == 'lines and points': self.griditem.setDrawlines(True) elif value == 'points': self.griditem.setDrawlines(False) elif value == 'circles': pass # self.griditem.setGridAppearance(False) else: raise ValueError("unknown grid styling") ### ACCESSORS ### def boundingRect(self): """Summary Args: TYPE: Description """ return self._rect # end def def modelColor(self): """Summary Args: TYPE: Description """ return self._model_props['color'] # end def def window(self): """Summary Args: TYPE: Description """ return self.parentItem().window() # end def def setActiveVirtualHelixItem(self, new_active_vhi): """Summary Args: new_active_vhi (TYPE): Description """ current_vhi = self.active_virtual_helix_item if new_active_vhi != current_vhi: if current_vhi is not None: current_vhi.deactivate() if new_active_vhi is not None: new_active_vhi.activate() self.active_virtual_helix_item = new_active_vhi # end def def setPreXoverItemsVisible(self, virtual_helix_item): """ self._pre_xover_items list references prexovers parented to other PathHelices such that only the activeHelix maintains the list of visible prexovers Args: virtual_helix_item (cadnano.views.sliceview.virtualhelixitem.SliceVirtualHelixItem): Description """ vhi = virtual_helix_item pxom = self.prexover_manager if vhi is None: pxom.hideGroups() return part = self.part() info = part.active_base_info if info: id_num, is_fwd, idx, to_vh_id_num = info per_neighbor_hits, pairs = part.potentialCrossoverMap(id_num, idx) pxom.activateVirtualHelix(virtual_helix_item, idx, per_neighbor_hits, pairs) # end def def removeVirtualHelixItem(self, id_num): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. Args: TYPE: Description """ vhi = self._virtual_helix_item_hash[id_num] if vhi == self.active_virtual_helix_item: self.active_virtual_helix_item = None vhi.virtualHelixRemovedSlot() del self._virtual_helix_item_hash[id_num] # When any VH is removed, turn SPA mode off self.shortest_path_add_mode = False self.shortest_path_start = None # end def def reconfigureRect(self, top_left, bottom_right, finish=False, padding=80): """Reconfigures the rectangle that is the document. Args: top_left (tuple): A tuple corresponding to the x-y coordinates of top left corner of the document bottom_right (tuple): A tuple corresponding to the x-y coordinates of the bottom left corner of the document Returns: tuple: tuple of point tuples representing the top_left and bottom_right as reconfigured with padding """ rect = self._rect ptTL = QPointF( *self.padTL(padding, *top_left)) if top_left else rect.topLeft() ptBR = QPointF(*self.padBR( padding, *bottom_right)) if bottom_right else rect.bottomRight() self._rect = QRectF(ptTL, ptBR) self.setRect(self._rect) self._configureOutline(self.outline) self.griditem.updateGrid() return self.outline.rect() # end def def padTL(self, padding, xTL, yTL): return xTL + padding, yTL + padding # end def def padBR(self, padding, xBR, yBR): return xBR - padding, yBR - padding # end def def enlargeRectToFit(self): """Enlarges Part Rectangle to fit the model bounds. This should be called when adding a SliceVirtualHelixItem. This method enlarges the rectangle to ensure that it fits the design. This method needs to check the model size to do this, but also takes into account any expansions the user has made to the rectangle as to not shrink the rectangle after the user has expanded it. :rtype: None """ padding = self._BOUNDING_RECT_PADDING model_left, model_top, model_right, model_bottom = self.getModelMinBounds( ) rect_left, rect_right, rect_bottom, rect_top = self.bounds() xTL = min(rect_left, model_left) - padding xBR = max(rect_right, model_right) + padding yTL = min(rect_top, model_top) - padding yBR = max(rect_bottom, model_bottom) + padding new_outline_rect = self.reconfigureRect(top_left=(xTL, yTL), bottom_right=(xBR, yBR)) self.resize_handle_group.alignHandles(new_outline_rect) # self.grab_cornerTL.alignPos(*top_left) # self.grab_cornerBR.alignPos(*bottom_right) ### PRIVATE SUPPORT METHODS ### def _configureOutline(self, outline): """Adjusts `outline` size with default padding. Args: outline (TYPE): Description Returns: o_rect (QRect): `outline` rect adjusted by _BOUNDING_RECT_PADDING """ _p = self._BOUNDING_RECT_PADDING o_rect = self.rect().adjusted(-_p, -_p, _p, _p) outline.setRect(o_rect) return o_rect # end def def boundRectToModel(self): """Update the size of the rectangle corresponding to the grid to the size of the model or a minimum size (whichever is greater). :rtype: None """ xTL, yTL, xBR, yBR = self.getModelMinBounds() self._rect = QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR)) # end def def getModelMinBounds(self, handle_type=None): """Bounds in form of Qt scaled from model Args: Tuple (top_left, bottom_right) :rtype: Tuple where """ xLL, yLL, xUR, yUR = self.part().boundDimensions(self.scale_factor) # return xLL, -yUR, xUR, -yLL r = self._RADIUS return xLL - r, -yUR - r, xUR + r, -yLL + r # end def def bounds(self): """x_low, x_high, y_low, y_high """ rect = self._rect return (rect.left(), rect.right(), rect.bottom(), rect.top()) ### PUBLIC SUPPORT METHODS ### def setLastHoveredItem(self, gridpoint_item): """Stores the last self-reported griditem to be hovered. Args: griditem (GridItem): the hoveree """ self._last_hovered_item = gridpoint_item def setModifyState(self, bool_val): """Hides the mod_rect when modify state disabled. Args: bool_val (boolean): what the modifystate should be set to. """ self._can_show_mod_circ = bool_val if bool_val is False: self._mod_circ.hide() # end def def showModelMinBoundsHint(self, handle_type, show=True): """Shows QGraphicsRectItem reflecting current model bounds. ResizeHandleGroup should toggle this when resizing. Args: status_str (str): Description to display in status bar. """ m_b_h = self.model_bounds_hint if show: xTL, yTL, xBR, yBR = self.getModelMinBounds() m_b_h.setRect(QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR))) m_b_h.show() else: m_b_h.hide() def updateStatusBar(self, status_str): """Shows status_str in the MainWindow's status bar. Args: status_str (str): Description to display in status bar. """ pass # disabled for now. # self.window().statusBar().showMessage(status_str, timeout) # end def def zoomToFit(self): """Ask the view to zoom to fit. """ thescene = self.scene() theview = thescene.views()[0] theview.zoomToFit() # end def ### EVENT HANDLERS ### def mousePressEvent(self, event): """Handler for user mouse press. Args: event (QGraphicsSceneMouseEvent): Contains item, scene, and screen coordinates of the the event, and previous event. Args: event (QMouseEvent): contains parameters that describe a mouse event. """ if event.button() == Qt.RightButton: return part = self._model_part part.setSelected(True) if self.isMovable(): return QGraphicsItem.mousePressEvent(self, event) tool = self._getActiveTool() if tool.FILTER_NAME not in part.document().filter_set: return tool_method_name = tool.methodPrefix() + "MousePress" if tool_method_name == 'createToolMousePress': return elif hasattr(self, tool_method_name): getattr(self, tool_method_name)(tool, event) else: event.setaccepted(False) QGraphicsItem.mousePressEvent(self, event) # end def def hoverMoveEvent(self, event): self.last_mouse_position = self.translateEventCoordinates(event) tool = self._getActiveTool() tool_method_name = tool.methodPrefix() + "HoverMove" if hasattr(self, tool_method_name): getattr(self, tool_method_name)(tool, event) else: event.setAccepted(False) QGraphicsItem.hoverMoveEvent(self, event) # def hoverLeaveEvent(self, event): # pass # tool = self._getActiveTool() # tool.hideLineItem() def getModelPos(self, pos): """Y-axis is inverted in Qt +y === DOWN Args: pos (TYPE): Description """ sf = self.scale_factor x, y = pos.x() / sf, -1.0 * pos.y() / sf return x, y # end def def getVirtualHelixItem(self, id_num): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. Returns: TYPE: Description """ return self._virtual_helix_item_hash.get(id_num) # end def def keyPressEvent(self, event): is_alt = bool(event.modifiers() & Qt.AltModifier) if event.key() == Qt.Key_Escape: self._setShortestPathStart(None) self.removeAllCreateHints() if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): self.highlightOneGridPoint(self.getLastHoveredCoordinates()) elif is_alt and self.shortest_path_add_mode is True: if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): x, y = self.coordinates_to_xy.get( self.getLastHoveredCoordinates()) self._preview_spa((x, y)) elif is_alt: if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): self.highlightOneGridPoint(self.getLastHoveredCoordinates(), styles.SPA_START_HINT_COLOR) # end def def keyReleaseEvent(self, event): is_alt = bool(event.modifiers() & Qt.AltModifier) if not is_alt: self.removeAllCreateHints() if self._inPointItem(self.last_mouse_position, self.getLastHoveredCoordinates()): self.highlightOneGridPoint(self.getLastHoveredCoordinates()) # end def def createToolMousePress(self, tool, event, alt_event=None): """Creates individual or groups of VHs in Part on user input. Shift modifier enables multi-helix addition. Args: event (TYPE): Description alt_event (None, optional): Description """ # Abort if a VH already exists here position = self.translateEventCoordinates(event) # 1. get point in model coordinates: part = self._model_part if alt_event is None: pt = tool.eventToPosition(self, event) else: pt = alt_event.pos() if pt is None: tool.deactivate() return QGraphicsItem.mousePressEvent(self, event) part_pt_tuple = self.getModelPos(pt) modifiers = event.modifiers() is_spa_mode = modifiers == Qt.AltModifier last_added_spa_vhi_id = self._handleShortestPathMousePress( tool=tool, position=position, is_spa_mode=is_spa_mode) if last_added_spa_vhi_id is not None: return row, column = self.getLastHoveredCoordinates() parity = self._getCoordinateParity(row, column) part.createVirtualHelix(x=part_pt_tuple[0], y=part_pt_tuple[1], parity=parity) id_num = part.getVirtualHelixAtPoint(part_pt_tuple) vhi = self._virtual_helix_item_hash[id_num] tool.setVirtualHelixItem(vhi) tool.startCreation() if is_spa_mode: self._highlightSpaVH(id_num) # end def def _getCoordinateParity(self, row, column): if self.griditem.grid_type is GridType.HONEYCOMB: return 0 if HoneycombDnaPart.isOddParity(row=row, column=column) else 1 elif self.griditem.grid_type is GridType.SQUARE: return 0 if SquareDnaPart.isEvenParity(row=row, column=column) else 1 else: return None def _handleShortestPathMousePress(self, tool, position, is_spa_mode): """ Handles logic for determining if SPA mode should be activated or continued. Args: tool (): position (tuple): the xy coordinates of the mouse press is_spa_mode (bool): whether or not this event is a SPA event Returns: True if nothing needs to be done by the caller (i.e. this method and its callees added VHs as necessary, False otherwise """ if is_spa_mode: # Complete the path if self.shortest_path_start is not None: last_vhi_id = self.createToolShortestPath( tool=tool, start=self.shortest_path_start, end=position) if last_vhi_id is not None: self._setShortestPathStart(position) self._highlightSpaVH(last_vhi_id) return last_vhi_id # Initialize SPA else: self._setShortestPathStart(position) else: self._setShortestPathStart(None) def _setShortestPathStart(self, position): # TODO[NF]: Docstring if position is not None: self.shortest_path_add_mode = True self.shortest_path_start = position else: self.shortest_path_add_mode = False self.shortest_path_start = None self._highlightSpaVH(None) def _highlightSpaVH(self, vh_id): # TODO[NF]: Docstring if self.spa_start_vhi: self.spa_start_vhi.setBrush(getNoBrush()) if vh_id is None: self.spa_start_vhi = None else: self.spa_start_vhi = self._virtual_helix_item_hash[vh_id] self.spa_start_vhi.setBrush( getBrushObj(styles.SPA_START_HINT_COLOR, alpha=32)) # end def def createToolShortestPath(self, tool, start, end): """ Handle the creation of VHIs for SPA mode. Args: tool (): start (tuple): the x-y coordinates of the start point end (tuple): the x-y coordinates of the end point Returns: The ID of the last VHI created """ path = ShortestPathHelper.shortestPathXY( start=start, end=end, neighbor_map=self.neighbor_map, vh_set=self.coordinates_to_vhid.keys(), point_map=self.coordinates_to_xy, grid_type=self.griditem.grid_type, scale_factor=self.inverse_scale_factor, radius=self._RADIUS) # Abort and exit SPA if there is no path from start to end if path == []: self.shortest_path_start = None self.shortest_path_add_mode = False return None else: x_list, y_list, parity_list = zip(*path) id_numbers = self._model_part.batchCreateVirtualHelices( x_list=x_list, y_list=y_list, parity=parity_list) for id_number in id_numbers: vhi = self._virtual_helix_item_hash[id_number] tool.setVirtualHelixItem(vhi) tool.startCreation() return id_number # end def def createToolHoverMove(self, tool, event): """Summary Args: tool (TYPE): Description event (TYPE): Description Returns: TYPE: Description """ event_xy = self.translateEventCoordinates(event) event_coord = ShortestPathHelper.findClosestPoint( event_xy, self.coordinates_to_xy) is_alt = True if event.modifiers() & Qt.AltModifier else False self.last_mouse_position = event_xy # Un-highlight GridItems if necessary by calling createToolHoverLeave if len(self._highlighted_path) > 1 or ( self._highlighted_path and self._highlighted_path[0] != event_coord): self.createToolHoverLeave(tool=tool, event=event) # Highlight GridItems if alt is being held down if is_alt and self.shortest_path_add_mode and self._inPointItem( event_xy, event_coord): self._preview_spa(event_xy) else: point_item = self.coordinates_to_xy.get(event_coord) if point_item is not None and self._inPointItem( event_xy, event_coord) and is_alt: self.highlightOneGridPoint(self.getLastHoveredCoordinates(), styles.SPA_START_HINT_COLOR) elif point_item is not None and self._inPointItem( event_xy, event_coord) and not is_alt: part = self._model_part next_idnums = (part._getNewIdNum(0), part._getNewIdNum(1)) self.griditem.showCreateHint(event_coord, next_idnums=next_idnums) self._highlighted_path.append(event_coord) tool.hoverMoveEvent(self, event) return QGraphicsItem.hoverMoveEvent(self, event) # end def def _inPointItem(self, event_xy, event_coord): """ Determine if x-y coordinates are inside the given GridPoint. Args: event_xy (tuple): the x-y coordinates corresponding to the position of the mouse event_coord (tuple): the i-j coordinates corresponding to the location of the GridPoint Returns: True if the mouse is in the given GridPoint, False otherwise """ if event_xy is None or event_coord is None: return False point_x, point_y = self.coordinates_to_xy.get(event_coord) event_x, event_y = event_xy return (point_x - event_x)**2 + (point_y - event_y)**2 <= self._RADIUS**2 # end def def _preview_spa(self, event_xy): """ Highlight and add VH ID numbers to the GridPoints that the SPA would use. Args: event_xy (tuple): the x-y coordinates corresponding to the position of the mouse Returns: None """ part = self._model_part start_coord = self.shortest_path_start end_coord = event_xy self._highlighted_path = ShortestPathHelper.shortestPathAStar( start=start_coord, end=end_coord, neighbor_map=self.neighbor_map, vh_set=self.coordinates_to_vhid.keys(), point_map=self.coordinates_to_xy) even_id = part._getNewIdNum(0) odd_id = part._getNewIdNum(1) for coord in self._highlighted_path: is_odd = self.griditem.showCreateHint(coord, next_idnums=(even_id, odd_id)) if is_odd is True: odd_id += 2 elif is_odd is False: even_id += 2 # end def def createToolHoverLeave(self, tool, event): self.removeAllCreateHints() def selectToolMousePress(self, tool, event): """ Args: tool (TYPE): Description event (TYPE): Description """ tool.setPartItem(self) pt = tool.eventToPosition(self, event) part_pt_tuple = self.getModelPos(pt) part = self._model_part if part.isVirtualHelixNearPoint(part_pt_tuple): id_num = part.getVirtualHelixAtPoint(part_pt_tuple) if id_num is not None: pass # loc = part.getCoordinate(id_num, 0) # print("VirtualHelix #{} at ({:.3f}, {:.3f})".format(id_num, loc[0], loc[1])) else: # tool.deselectItems() tool.modelClear() else: # tool.deselectItems() tool.modelClear() return QGraphicsItem.mousePressEvent(self, event) # end def def setNeighborMap(self, neighbor_map): """ Update the internal mapping of coordinates to their neighbors. Args: neighbor_map (dict): the new mapping of coordinates to their neighbors Returns: None """ assert isinstance(neighbor_map, dict) self.neighbor_map = neighbor_map # end def def setPointMap(self, point_map): """ Update the internal mapping of coordinates to x-y positions. Args: point_map (dict): the new mapping of coordinates to their x-y position Returns: None """ assert isinstance(point_map, dict) self.coordinates_to_xy = point_map # end def def updateTranslatedOffsets(self, delta_x, delta_y): """ Update the values used to calculate translational offsets. Args: delta_x (float): the new value for which we've translated in the x direction delta_y (float): the new value for which we've translated in the y direction Returns: None """ assert isinstance(delta_x, float) assert isinstance(delta_y, float) self._translated_x = delta_x self._translated_y = delta_y # end def def translateEventCoordinates(self, event): """ Given an event, return the x-y coordinates of the event accounting for any translations that may have happened Args: event (MousePressEvent): the event for which x-y coordinates should be returned Returns: A tuple of x-y coordinates of the event """ assert isinstance(event, QGraphicsSceneEvent) return event.scenePos().x() - self._translated_x, event.scenePos().y( ) - self._translated_y # end def def removeAllCreateHints(self): """ Remove the create hints from each currently hinted GridItem. Iterates over all coordinates in self._highlighted_path. Returns: None """ for coord in self._highlighted_path: self.griditem.showCreateHint(coord, show_hint=False) self._highlighted_path = [] # end def def highlightOneGridPoint(self, coordinates, color=None): """ Add a hint to one GridPoint. Args: coordinates (tuple): the row-column coordinates of the gridPoint to be highlighted color (): the color that the gridPoint should be changed to Returns: None """ if coordinates is None: return assert isinstance(coordinates, tuple) and len(coordinates) is 2 assert isinstance(coordinates[0], int) and isinstance( coordinates[1], int) if self.coordinates_to_xy.get(coordinates) is not None: part = self._model_part next_idnums = (part._getNewIdNum(0), part._getNewIdNum(1)) self.griditem.showCreateHint(coordinates, next_idnums=next_idnums, color=color) self._highlighted_path.append(coordinates) # end def def getLastHoveredCoordinates(self): """ Get the row and column corresponding to the GridPoint that was most recently hovered over. This accounts for the fact that the rows are inverted (i.e. the result returned by this method will match the coordinate system stored in this class' internal records of coordinates) Returns: A tuple corresponding to the row and column of the most recently hovered GridPoint. """ if self._last_hovered_item: row, column = self._last_hovered_item.coord() return -row, column
class PathWorkplaneItem(QGraphicsRectItem): """Draws the rectangle to indicate the current Workplane, i.e. the region of part bases affected by certain actions in other views.""" _BOUNDING_RECT_PADDING = 0 _HANDLE_SIZE = 6 _MIN_WIDTH = 3 def __init__(self, model_part: NucleicAcidPartT, part_item: PathNucleicAcidPartItemT): """ Args: model_part: part_item: """ super(QGraphicsRectItem, self).__init__(BASE_RECT, part_item.proxy()) self.setAcceptHoverEvents(True) self.setBrush(getBrushObj(styles.BLUE_FILL, alpha=12)) self.setPen(getNoPen()) self.setZValue(styles.ZWORKPLANE) self._model_part = model_part self._part_item = part_item self._low_drag_bound = 0 # idx, not pos self._high_drag_bound = model_part.getProperty( 'max_vhelix_length') # idx, not pos self._moving_via_handle = False self.outline = PathWorkplaneOutline(self) self.resize_handle_group = ResizeHandleGroup(self.rect(), self._HANDLE_SIZE, styles.BLUE_STROKE, True, HandleEnum.LEFT | HandleEnum.RIGHT, self, translates_in=AxisEnum.X) # Minimum size hint (darker internal rect, visible during resizing) self.model_bounds_hint = m_b_h = QGraphicsRectItem(self) m_b_h.setBrush(getBrushObj(styles.BLUE_FILL, alpha=64)) m_b_h.setPen(getNoPen()) m_b_h.hide() # Low and high idx labels self.resize_handle_group.updateText(HandleEnum.LEFT, self._idx_low) self.resize_handle_group.updateText(HandleEnum.RIGHT, self._idx_high) # end def def getModelMinBounds(self, handle_type: EnumType = None) -> RectT: """Resize bounds in form of Qt position, scaled from model.""" # TODO: fix bug preventing dragging in imported files if handle_type and handle_type & HandleEnum.LEFT: xTL = (self._idx_high - self._MIN_WIDTH) * BASE_WIDTH xBR = self._idx_high * BASE_WIDTH elif handle_type and handle_type & HandleEnum.RIGHT: xTL = (self._idx_low + self._MIN_WIDTH) * BASE_WIDTH xBR = (self._idx_low) * BASE_WIDTH else: # default to HandleEnum.RIGHT behavior for all types print("No HandleEnum?") xTL = 0 xBR = self._high_drag_bound * BASE_WIDTH yTL = self._part_item._vh_rect.top() yBR = self._part_item._vh_rect.bottom() - BASE_WIDTH * 3 return xTL, yTL, xBR, yBR # end def def setMovable(self, is_movable: bool): """ Args: is_movable: is this movable? """ self._moving_via_handle = is_movable # self.setFlag(QGraphicsItem.ItemIsMovable, is_movable) # end def def finishDrag(self): """Set the workplane size in the model""" pass # pos = self.pos() # position = pos.x(), pos.y() # view_name = self._viewroot.name # self._model_part.changeInstanceProperty(self._model_instance, view_name, 'position', position) # end def def reconfigureRect(self, top_left: Vec2T, bottom_right: Vec2T, finish: bool = False) -> QRectF: """Update the workplane rect to draw from top_left to bottom_right, snapping the x values to the nearest base width. Updates the outline and resize handles. Args: top_left: topLeft (x, y) bottom_right: bottomRight (x, y) Returns: the new rect. """ if top_left: xTL = max(top_left[0], self._low_drag_bound) xTL = xTL - (xTL % BASE_WIDTH) # snap to nearest base self._idx_low = int(xTL / BASE_WIDTH) self.resize_handle_group.updateText(HandleEnum.LEFT, self._idx_low) else: xTL = self._idx_low * BASE_WIDTH if bottom_right: xBR = util.clamp(bottom_right[0], (self._idx_low + self._MIN_WIDTH) * BASE_WIDTH, (self._high_drag_bound) * BASE_WIDTH) xBR = xBR - (xBR % BASE_WIDTH) # snap to nearest base self._idx_high = int(xBR / BASE_WIDTH) self.resize_handle_group.updateText(HandleEnum.RIGHT, self._idx_high) else: xBR = self._idx_high * BASE_WIDTH yTL = self._part_item._vh_rect.top() yBR = self._part_item._vh_rect.bottom() - BASE_WIDTH * 3 self.setRect(QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR))) self.outline.setRect(self.rect()) self.outline.updateAppearance() self.resize_handle_group.alignHandles(self.rect()) self._model_part.setProperty('workplane_idxs', (self._idx_low, self._idx_high), use_undostack=False) return self.rect() def setIdxs(self, new_idxs: SegmentT): if self._idx_low != new_idxs[0] or self._idx_high != new_idxs[1]: self._idx_low = new_idxs[0] self._idx_high = new_idxs[1] self.reconfigureRect((), ()) self._high_drag_bound = self._model_part.getProperty( 'max_vhelix_length') # end def def showModelMinBoundsHint(self, handle_type: EnumType, show: bool = True): m_b_h = self.model_bounds_hint if show: xTL, yTL, xBR, yBR = self.getModelMinBounds(handle_type) m_b_h.setRect(QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR))) m_b_h.show() else: m_b_h.hide() self._part_item.update() # m_b_h hangs around unless force repaint # end def def width(self) -> int: """ Returns: width of the PathWorkplaneItem in index distance """ return self._idx_high - self._idx_low # end def ### EVENT HANDLERS ### def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): if event.modifiers() & Qt.ShiftModifier: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.ArrowCursor) self._part_item.updateStatusBar("{}–{}".format(self._idx_low, self._idx_high)) QGraphicsItem.hoverEnterEvent(self, event) # end def def hoverMoveEvent(self, event: QGraphicsSceneHoverEvent): if event.modifiers() & Qt.ShiftModifier: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.ArrowCursor) QGraphicsItem.hoverMoveEvent(self, event) # end def def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): self.setCursor(Qt.ArrowCursor) self._part_item.updateStatusBar("") QGraphicsItem.hoverLeaveEvent(self, event) # end def def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """Parses a mousePressEvent. Stores _move_idx and _offset_idx for future comparison. """ self._high_drag_bound = self._model_part.getProperty( 'max_vhelix_length') - self.width() if event.modifiers() & Qt.ShiftModifier or self._moving_via_handle: self.setCursor(Qt.ClosedHandCursor) self._start_idx_low = self._idx_low self._start_idx_high = self._idx_high self._delta = 0 self._move_idx = int( floor((self.x() + event.pos().x()) / BASE_WIDTH)) self._offset_idx = int(floor(event.pos().x()) / BASE_WIDTH) else: return QGraphicsItem.mousePressEvent(self, event) # end def def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """Converts event coords into an idx delta and updates if changed. """ delta = int(floor( (self.x() + event.pos().x()) / BASE_WIDTH)) - self._offset_idx delta = util.clamp( delta, self._low_drag_bound - self._start_idx_low, self._high_drag_bound - self._start_idx_high + self.width()) if self._delta != delta: self._idx_low = int(self._start_idx_low + delta) self._idx_high = int(self._start_idx_high + delta) self._delta = delta self.reconfigureRect((), ()) self.resize_handle_group.updateText(HandleEnum.LEFT, self._idx_low) self.resize_handle_group.updateText(HandleEnum.RIGHT, self._idx_high) return QGraphicsItem.mouseMoveEvent(self, event) # end def def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): """Repeates mouseMove calculation in case any new movement. """ self.setCursor(Qt.ArrowCursor) delta = int(floor( (self.x() + event.pos().x()) / BASE_WIDTH)) - self._offset_idx delta = util.clamp( delta, self._low_drag_bound - self._start_idx_low, self._high_drag_bound - self._start_idx_high + self.width()) if self._delta != delta: self._idx_low = int(self._start_idx_low + delta) self._idx_high = int(self._start_idx_high + delta) self._delta = delta self.reconfigureRect((), ()) self._high_drag_bound = self._model_part.getProperty( 'max_vhelix_length') # reset for handles return QGraphicsItem.mouseReleaseEvent(self, event)
class PathWorkplaneItem(QGraphicsRectItem): """Draws the rectangle to indicate the current Workplane, i.e. the region of part bases affected by certain actions in other views.""" _BOUNDING_RECT_PADDING = 0 _HANDLE_SIZE = 6 _MIN_WIDTH = 3 def __init__(self, model_part: NucleicAcidPartT, part_item: PathNucleicAcidPartItemT): """ Args: model_part: part_item: """ super(QGraphicsRectItem, self).__init__(BASE_RECT, part_item.proxy()) self.setAcceptHoverEvents(True) self.setBrush(getBrushObj(styles.BLUE_FILL, alpha=12)) self.setPen(getNoPen()) self.setZValue(styles.ZWORKPLANE) self._model_part = model_part self._part_item = part_item self._low_drag_bound = 0 # idx, not pos self._high_drag_bound = model_part.getProperty('max_vhelix_length') # idx, not pos self._moving_via_handle = False self.outline = PathWorkplaneOutline(self) self.resize_handle_group = ResizeHandleGroup(self.rect(), self._HANDLE_SIZE, styles.BLUE_STROKE, True, HandleEnum.LEFT | HandleEnum.RIGHT, self, translates_in=AxisEnum.X) # Minimum size hint (darker internal rect, visible during resizing) self.model_bounds_hint = m_b_h = QGraphicsRectItem(self) m_b_h.setBrush(getBrushObj(styles.BLUE_FILL, alpha=64)) m_b_h.setPen(getNoPen()) m_b_h.hide() # Low and high idx labels self.resize_handle_group.updateText(HandleEnum.LEFT, self._idx_low) self.resize_handle_group.updateText(HandleEnum.RIGHT, self._idx_high) # end def def getModelMinBounds(self, handle_type: EnumType = None) -> RectT: """Resize bounds in form of Qt position, scaled from model.""" # TODO: fix bug preventing dragging in imported files if handle_type and handle_type & HandleEnum.LEFT: xTL = (self._idx_high-self._MIN_WIDTH)*BASE_WIDTH xBR = self._idx_high*BASE_WIDTH elif handle_type and handle_type & HandleEnum.RIGHT: xTL = (self._idx_low+self._MIN_WIDTH)*BASE_WIDTH xBR = (self._idx_low)*BASE_WIDTH else: # default to HandleEnum.RIGHT behavior for all types print("No HandleEnum?") xTL = 0 xBR = self._high_drag_bound*BASE_WIDTH yTL = self._part_item._vh_rect.top() yBR = self._part_item._vh_rect.bottom()-BASE_WIDTH*3 return xTL, yTL, xBR, yBR # end def def setMovable(self, is_movable: bool): """ Args: is_movable: is this movable? """ self._moving_via_handle = is_movable # self.setFlag(QGraphicsItem.ItemIsMovable, is_movable) # end def def finishDrag(self): """Set the workplane size in the model""" pass # pos = self.pos() # position = pos.x(), pos.y() # view_name = self._viewroot.name # self._model_part.changeInstanceProperty(self._model_instance, view_name, 'position', position) # end def def reconfigureRect(self, top_left: Vec2T, bottom_right: Vec2T, finish: bool = False) -> QRectF: """Update the workplane rect to draw from top_left to bottom_right, snapping the x values to the nearest base width. Updates the outline and resize handles. Args: top_left: topLeft (x, y) bottom_right: bottomRight (x, y) Returns: the new rect. """ if top_left: xTL = max(top_left[0], self._low_drag_bound) xTL = xTL - (xTL % BASE_WIDTH) # snap to nearest base self._idx_low = int(xTL/BASE_WIDTH) self.resize_handle_group.updateText(HandleEnum.LEFT, self._idx_low) else: xTL = self._idx_low*BASE_WIDTH if bottom_right: xBR = util.clamp(bottom_right[0], (self._idx_low+self._MIN_WIDTH)*BASE_WIDTH, (self._high_drag_bound)*BASE_WIDTH) xBR = xBR - (xBR % BASE_WIDTH) # snap to nearest base self._idx_high = int(xBR/BASE_WIDTH) self.resize_handle_group.updateText(HandleEnum.RIGHT, self._idx_high) else: xBR = self._idx_high*BASE_WIDTH yTL = self._part_item._vh_rect.top() yBR = self._part_item._vh_rect.bottom()-BASE_WIDTH*3 self.setRect(QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR))) self.outline.setRect(self.rect()) self.outline.updateAppearance() self.resize_handle_group.alignHandles(self.rect()) self._model_part.setProperty('workplane_idxs', (self._idx_low, self._idx_high), use_undostack=False) return self.rect() def setIdxs(self, new_idxs: SegmentT): if self._idx_low != new_idxs[0] or self._idx_high != new_idxs[1]: self._idx_low = new_idxs[0] self._idx_high = new_idxs[1] self.reconfigureRect((), ()) self._high_drag_bound = self._model_part.getProperty('max_vhelix_length') # end def def showModelMinBoundsHint(self, handle_type: EnumType, show: bool = True): m_b_h = self.model_bounds_hint if show: xTL, yTL, xBR, yBR = self.getModelMinBounds(handle_type) m_b_h.setRect(QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR))) m_b_h.show() else: m_b_h.hide() self._part_item.update() # m_b_h hangs around unless force repaint # end def def width(self) -> int: """ Returns: width of the PathWorkplaneItem in index distance """ return self._idx_high - self._idx_low # end def ### EVENT HANDLERS ### def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): if event.modifiers() & Qt.ShiftModifier: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.ArrowCursor) self._part_item.updateStatusBar("{}–{}".format(self._idx_low, self._idx_high)) QGraphicsItem.hoverEnterEvent(self, event) # end def def hoverMoveEvent(self, event: QGraphicsSceneHoverEvent): if event.modifiers() & Qt.ShiftModifier: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.ArrowCursor) QGraphicsItem.hoverMoveEvent(self, event) # end def def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): self.setCursor(Qt.ArrowCursor) self._part_item.updateStatusBar("") QGraphicsItem.hoverLeaveEvent(self, event) # end def def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """Parses a mousePressEvent. Stores _move_idx and _offset_idx for future comparison. """ self._high_drag_bound = self._model_part.getProperty('max_vhelix_length') - self.width() if event.modifiers() & Qt.ShiftModifier or self._moving_via_handle: self.setCursor(Qt.ClosedHandCursor) self._start_idx_low = self._idx_low self._start_idx_high = self._idx_high self._delta = 0 self._move_idx = int(floor((self.x()+event.pos().x()) / BASE_WIDTH)) self._offset_idx = int(floor(event.pos().x()) / BASE_WIDTH) else: return QGraphicsItem.mousePressEvent(self, event) # end def def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """Converts event coords into an idx delta and updates if changed. """ delta = int(floor((self.x()+event.pos().x()) / BASE_WIDTH)) - self._offset_idx delta = util.clamp(delta, self._low_drag_bound-self._start_idx_low, self._high_drag_bound-self._start_idx_high+self.width()) if self._delta != delta: self._idx_low = int(self._start_idx_low + delta) self._idx_high = int(self._start_idx_high + delta) self._delta = delta self.reconfigureRect((), ()) self.resize_handle_group.updateText(HandleEnum.LEFT, self._idx_low) self.resize_handle_group.updateText(HandleEnum.RIGHT, self._idx_high) return QGraphicsItem.mouseMoveEvent(self, event) # end def def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): """Repeates mouseMove calculation in case any new movement. """ self.setCursor(Qt.ArrowCursor) delta = int(floor((self.x()+event.pos().x()) / BASE_WIDTH)) - self._offset_idx delta = util.clamp(delta, self._low_drag_bound-self._start_idx_low, self._high_drag_bound-self._start_idx_high+self.width()) if self._delta != delta: self._idx_low = int(self._start_idx_low + delta) self._idx_high = int(self._start_idx_high + delta) self._delta = delta self.reconfigureRect((), ()) self._high_drag_bound = self._model_part.getProperty('max_vhelix_length') # reset for handles return QGraphicsItem.mouseReleaseEvent(self, event)
class PathNucleicAcidPartItem(QAbstractPartItem): """Summary Attributes: active_virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): Description findChild (TYPE): Description grab_corner (TYPE): Description prexover_manager (TYPE): Description """ findChild = util.findChild # for debug _BOUNDING_RECT_PADDING = 20 _GC_SIZE = 10 def __init__(self, model_part_instance, viewroot, parent): """parent should always be pathrootitem Args: model_part_instance (TYPE): Description viewroot (TYPE): Description parent (TYPE): Description """ super(PathNucleicAcidPartItem, self).__init__(model_part_instance, viewroot, parent) self.setAcceptHoverEvents(True) self._getActiveTool = viewroot.manager.activeToolGetter self.active_virtual_helix_item = None m_p = self._model_part self._controller = NucleicAcidPartItemController(self, m_p) self.prexover_manager = PreXoverManager(self) self._virtual_helix_item_list = [] self._initModifierRect() self._proxy_parent = ProxyParentItem(self) self._proxy_parent.setFlag(QGraphicsItem.ItemHasNoContents) self._scale_2_model = m_p.baseWidth() / _BASE_WIDTH self._scale_2_Qt = _BASE_WIDTH / m_p.baseWidth() # self._rect = QRectF() self._vh_rect = QRectF() # self.setPen(getPenObj(styles.ORANGE_STROKE, 0)) self.setPen(getNoPen()) # self.setRect(self._rect) self.outline = outline = PathRectItem(self) outline.setFlag(QGraphicsItem.ItemStacksBehindParent) self.setZValue(styles.ZPART) self._proxy_parent.setZValue(styles.ZPART) outline.setZValue(styles.ZDESELECTOR) self.outline.setPen(getPenObj(m_p.getColor(), _DEFAULT_WIDTH)) o_rect = self._configureOutline(outline) model_color = m_p.getColor() self.resize_handle_group = ResizeHandleGroup( o_rect, _HANDLE_SIZE, model_color, True, # HandleType.LEFT | HandleType.RIGHT, self) self.model_bounds_hint = m_b_h = QGraphicsRectItem(self) m_b_h.setBrush(getBrushObj(styles.BLUE_FILL, alpha=32)) m_b_h.setPen(getNoPen()) m_b_h.hide() self.workplane = PathWorkplaneItem(m_p, self) self.hide() # show on adding first vh # end def def proxy(self): """Summary Returns: TYPE: Description """ return self._proxy_parent # end def def modelColor(self): """Summary Returns: TYPE: Description """ return self._model_part.getProperty('color') # end def def convertToModelZ(self, z): """scale Z-axis coordinate to the model Args: z (TYPE): Description """ return z * self._scale_2_model # end def def convertToQtZ(self, z): """Summary Args: z (TYPE): Description Returns: TYPE: Description """ return z * self._scale_2_Qt # end def def _initModifierRect(self): """docstring for _initModifierRect """ self._can_show_mod_rect = False self._mod_rect = m_r = QGraphicsRectItem(_DEFAULT_RECT, self) m_r.setPen(_MOD_PEN) m_r.hide() # end def def vhItemForIdNum(self, id_num): """Returns the pathview VirtualHelixItem corresponding to id_num Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. """ return self._virtual_helix_item_hash.get(id_num) ### SIGNALS ### ### SLOTS ### def partActiveVirtualHelixChangedSlot(self, part, id_num): """Summary Args: part (TYPE): Description id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. Returns: TYPE: Description """ vhi = self._virtual_helix_item_hash.get(id_num, None) self.setActiveVirtualHelixItem(vhi) self.setPreXoverItemsVisible(vhi) # end def def partActiveBaseInfoSlot(self, part, info): """Summary Args: part (TYPE): Description info (TYPE): Description Returns: TYPE: Description """ pxi_m = self.prexover_manager pxi_m.deactivateNeighbors() if info and info is not None: id_num, is_fwd, idx, to_vh_id_num = info pxi_m.activateNeighbors(id_num, is_fwd, idx) # end def def partZDimensionsChangedSlot(self, model_part, min_id_num, max_id_num, ztf=False): """Summary Args: model_part (Part): The model part min_id_num (TYPE): Description max_id_num (TYPE): Description ztf (bool, optional): Description Returns: TYPE: Description """ if len(self._virtual_helix_item_list) > 0: vhi_hash = self._virtual_helix_item_hash vhi_max = vhi_hash[max_id_num] vhi_rect_max = vhi_max.boundingRect() self._vh_rect.setRight(vhi_rect_max.right() + vhi_max.x()) vhi_min = vhi_hash[min_id_num] vhi_h_rect = vhi_min.handle().boundingRect() self._vh_rect.setLeft( (vhi_h_rect.left() - styles.VH_XOFFSET + vhi_min.x())) if ztf: self.scene().views()[0].zoomToFit() TLx, TLy, BRx, BRy = self._getVHRectCorners() self.reconfigureRect((TLx, TLy), (BRx, BRy)) # end def def partSelectedChangedSlot(self, model_part, is_selected): """Summary Args: model_part (Part): The model part is_selected (TYPE): Description Returns: TYPE: Description """ # print("partSelectedChangedSlot", is_selected) if is_selected: self.resetPen(styles.SELECTED_COLOR, styles.SELECTED_PEN_WIDTH) self.resetBrush(styles.SELECTED_BRUSH_COLOR, styles.SELECTED_ALPHA) else: self.resetPen(self.modelColor()) self.resetBrush(styles.DEFAULT_BRUSH_COLOR, styles.DEFAULT_ALPHA) def partPropertyChangedSlot(self, model_part, property_key, new_value): """Summary Args: model_part (Part): The model part property_key (TYPE): Description new_value (TYPE): Description Returns: TYPE: Description """ if self._model_part == model_part: self._model_props[property_key] = new_value if property_key == 'color': for vhi in self._virtual_helix_item_list: vhi.handle().refreshColor() # self.workplane.outline.setPen(getPenObj(new_value, 0)) TLx, TLy, BRx, BRy = self._getVHRectCorners() self.reconfigureRect((TLx, TLy), (BRx, BRy)) elif property_key == 'is_visible': if new_value: self.show() else: self.hide() elif property_key == 'virtual_helix_order': vhi_dict = self._virtual_helix_item_hash new_list = [vhi_dict[id_num] for id_num in new_value] ztf = False self._setVirtualHelixItemList(new_list, zoom_to_fit=ztf) elif property_key == 'workplane_idxs': if hasattr(self, 'workplane'): self.workplane.setIdxs(new_idxs=new_value) # end def def partVirtualHelicesTranslatedSlot(self, sender, vh_set, left_overs, do_deselect): """Summary Args: sender (obj): Model object that emitted the signal. vh_set (TYPE): Description left_overs (TYPE): Description do_deselect (TYPE): Description Returns: TYPE: Description """ # self.prexover_manager.clearPreXoverItems() # if self.active_virtual_helix_item is not None: # self.active_virtual_helix_item.deactivate() # self.active_virtual_helix_item = None # if self.active_virtual_helix_item is not None: # self.setPreXoverItemsVisible(self.active_virtual_helix_item) pass # end def def partRemovedSlot(self, sender): """docstring for partRemovedSlot Args: sender (obj): Model object that emitted the signal. """ self.parentItem().removePartItem(self) scene = self.scene() scene.removeItem(self) self._model_part = None self._virtual_helix_item_hash = None self._virtual_helix_item_list = None self._controller.disconnectSignals() self._controller = None # self.grab_corner = None # end def def partVirtualHelixAddedSlot(self, model_part, id_num, virtual_helix, neighbors): """ When a virtual helix is added to the model, this slot handles the instantiation of a virtualhelix item. Args: model_part (Part): The model part id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. """ # print("NucleicAcidPartItem.partVirtualHelixAddedSlot") vhi = PathVirtualHelixItem(virtual_helix, self, self._viewroot) self._virtual_helix_item_hash[id_num] = vhi vhi_list = self._virtual_helix_item_list vhi_list.append(vhi) ztf = not getBatch() self._setVirtualHelixItemList(vhi_list, zoom_to_fit=ztf) if not self.isVisible(): self.show() # end def def partVirtualHelixResizedSlot(self, sender, id_num, virtual_helix): """Notifies the virtualhelix at coord to resize. Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. """ vhi = self._virtual_helix_item_hash[id_num] # print("resize:", id_num, virtual_helix.getSize()) vhi.resize() # end def def partVirtualHelixRemovingSlot(self, sender, id_num, virtual_helix, neighbors): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. Returns: TYPE: Description """ self.removeVirtualHelixItem(id_num) # end def def partVirtualHelixRemovedSlot(self, sender, id_num): """ Step 2 of removing a VHI """ ztf = not getBatch() self._setVirtualHelixItemList(self._virtual_helix_item_list, zoom_to_fit=ztf) if len(self._virtual_helix_item_list) == 0: self.hide() self.reconfigureRect((), ()) # end def def partVirtualHelixPropertyChangedSlot(self, sender, id_num, virtual_helix, keys, values): """Summary Args: sender (obj): Model object that emitted the signal. id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. keys (TYPE): Description values (TYPE): Description Returns: TYPE: Description """ if self._model_part == sender: vh_i = self._virtual_helix_item_hash[id_num] vh_i.virtualHelixPropertyChangedSlot(keys, values) # end def def partVirtualHelicesSelectedSlot(self, sender, vh_set, is_adding): """is_adding (bool): adding (True) virtual helices to a selection or removing (False) Args: sender (obj): Model object that emitted the signal. vh_set (TYPE): Description is_adding (TYPE): Description """ vhhi_group = self._viewroot.vhiHandleSelectionGroup() vh_hash = self._virtual_helix_item_hash doc = self._viewroot.document() if is_adding: # print("got the adding slot in path") for id_num in vh_set: vhi = vh_hash[id_num] vhhi = vhi.handle() vhhi.modelSelect(doc) # end for vhhi_group.processPendingToAddList() else: # print("got the removing slot in path") for id_num in vh_set: vhi = vh_hash[id_num] vhhi = vhi.handle() vhhi.modelDeselect(doc) # end for vhhi_group.processPendingToAddList() # end def ### ACCESSORS ### def removeVirtualHelixItem(self, id_num): """Summary Args: id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods. Returns: TYPE: Description """ self.setActiveVirtualHelixItem(None) vhi = self._virtual_helix_item_hash[id_num] vhi.virtualHelixRemovedSlot() self._virtual_helix_item_list.remove(vhi) del self._virtual_helix_item_hash[id_num] # end def def window(self): """Summary Returns: TYPE: Description """ return self.parentItem().window() # end def ### PRIVATE METHODS ### def _configureOutline(self, outline): """Adjusts `outline` size with default padding. Args: outline (TYPE): Description Returns: o_rect (QRect): `outline` rect adjusted by _BOUNDING_RECT_PADDING """ _p = self._BOUNDING_RECT_PADDING o_rect = self.rect().adjusted(-_p, -_p, _p, _p) outline.setRect(o_rect) return o_rect # end def def _getVHRectCorners(self): vhTL = self._vh_rect.topLeft() vhBR = self._vh_rect.bottomRight() # vhTLx, vhTLy = vhTL.x(), vhTL.y() # vhBRx, vhBRy = vhBR.x(), vhBR.y() return vhTL.x(), vhTL.y(), vhBR.x(), vhBR.y() # end def def _setVirtualHelixItemList(self, new_list, zoom_to_fit=True): """ Give me a list of VirtualHelixItems and I'll parent them to myself if necessary, position them in a column, adopt their handles, and position them as well. Args: new_list (TYPE): Description zoom_to_fit (bool, optional): Description """ y = 0 # How far down from the top the next PH should be vhi_rect = None vhi_h_rect = None vhi_h_selection_group = self._viewroot.vhiHandleSelectionGroup() for vhi in new_list: _, _, _z = vhi.cnModel().getAxisPoint(0) _z *= self._scale_2_Qt vhi.setPos(_z, y) if vhi_rect is None: vhi_rect = vhi.boundingRect() step = vhi_rect.height() + styles.PATH_HELIX_PADDING # end if # get the VirtualHelixHandleItem vhi_h = vhi.handle() do_reselect = False if vhi_h.parentItem() == vhi_h_selection_group: do_reselect = True vhi_h.tempReparent() # so positioning works if vhi_h_rect is None: vhi_h_rect = vhi_h.boundingRect() vhi_h_x = _z - _VH_XOFFSET vhi_h_y = y + (vhi_rect.height() - vhi_h_rect.height()) / 2 vhi_h.setPos(vhi_h_x, vhi_h_y) y += step self.updateXoverItems(vhi) if do_reselect: vhi_h_selection_group.addToGroup(vhi_h) # end for # this need only adjust top and bottom edges of the bounding rectangle # self._vh_rect.setTop() self._vh_rect.setBottom(y) self._virtual_helix_item_list = new_list # now update Z dimension (X in Qt space in the Path view) part = self.part() self.partZDimensionsChangedSlot(part, *part.zBoundsIds(), ztf=zoom_to_fit) # end def def resetPen(self, color, width=0): """Summary Args: color (TYPE): Description width (int, optional): Description Returns: TYPE: Description """ pen = getPenObj(color, width) self.outline.setPen(pen) # self.setPen(pen) # end def def resetBrush(self, color, alpha): """Summary Args: color (TYPE): Description alpha (TYPE): Description Returns: TYPE: Description """ brush = getBrushObj(color, alpha=alpha) self.setBrush(brush) # end def def reconfigureRect(self, top_left, bottom_right, finish=False, padding=80): """ Updates the bounding rect to the size of the childrenBoundingRect. Refreshes the outline and grab_corner locations. Called by partZDimensionsChangedSlot and partPropertyChangedSlot. """ outline = self.outline hasTL = True if top_left else False hasBR = True if bottom_right else False if hasTL ^ hasBR: # called via resizeHandle mouseMove? ptTL = QPointF(*top_left) if top_left else outline.rect().topLeft() ptBR = QPointF(*bottom_right) if bottom_right else outline.rect( ).bottomRight() o_rect = QRectF(ptTL, ptBR) pad_xoffset = self._BOUNDING_RECT_PADDING * 2 new_size = int( (o_rect.width() - _VH_XOFFSET - pad_xoffset) / _BASE_WIDTH) substep = self._model_part.subStepSize() snap_size = new_size - new_size % substep snap_offset = -(new_size % substep) * _BASE_WIDTH self.resize_handle_group.updateText(HandleType.RIGHT, snap_size) if finish: self._model_part.setAllVirtualHelixSizes(snap_size) o_rect = o_rect.adjusted(0, 0, snap_offset, 0) # print("finish", vh_size, new_size, substep, snap_size) self.outline.setRect(o_rect) else: # 1. Temporarily remove children that shouldn't affect size outline.setParentItem(None) self.workplane.setParentItem(None) self.model_bounds_hint.setParentItem(None) self.resize_handle_group.setParentItemAll(None) self.prexover_manager.setParentItem(None) # 2. Get the tight bounding rect self.setRect(self.childrenBoundingRect()) # vh_items only # 3. Restore children like nothing happened outline.setParentItem(self) self.workplane.setParentItem(self) self.model_bounds_hint.setParentItem(self) self.resize_handle_group.setParentItemAll(self) self.prexover_manager.setParentItem(self) self._configureOutline(outline) self.resetPen(self.modelColor(), 0) # cosmetic self.resetBrush(styles.DEFAULT_BRUSH_COLOR, styles.DEFAULT_ALPHA) self.workplane.reconfigureRect((), ()) self.resize_handle_group.alignHandles(outline.rect()) return outline.rect() # end def ### PUBLIC METHODS ### def getModelMinBounds(self, handle_type=None): """Bounds in form of Qt scaled from model Absolute min should be 2*stepsize. Round up from indexOfRightmostNonemptyBase to nearest substep. Returns: Tuple (xTL, yTL, xBR, yBR) """ _p = self._BOUNDING_RECT_PADDING default_idx = self._model_part.stepSize() * 2 nonempty_idx = self._model_part.indexOfRightmostNonemptyBase() right_bound_idx = max(default_idx, nonempty_idx) substep = self._model_part.subStepSize() snap_idx = (right_bound_idx / substep) * substep xTL = 0 xBR = snap_idx * _BASE_WIDTH + _p min_rect = self.rect().adjusted(-_p, -_p, _p, _p) yTL = min_rect.top() yBR = min_rect.bottom() return xTL, yTL, xBR, yBR # end def def showModelMinBoundsHint(self, handle_type, show=True): """Shows QGraphicsRectItem reflecting current model bounds. ResizeHandleGroup should toggle this when resizing. Args: status_str (str): Description to display in status bar. """ m_b_h = self.model_bounds_hint if show: xTL, yTL, xBR, yBR = self.getModelMinBounds() m_b_h.setRect(QRectF(QPointF(xTL, yTL), QPointF(xBR, yBR))) m_b_h.show() else: m_b_h.hide() # end def def setModifyState(self, bool): """Hides the modRect when modify state disabled. Args: bool (TYPE): Description """ self._can_show_mod_rect = bool if bool is False: self._mod_rect.hide() def getOrderedVirtualHelixList(self): """Used for encoding. """ ret = [] for vhi in self._virtual_helix_item_list: ret.append(vhi.coord()) return ret # end def def reorderHelices(self, id_nums, index_delta): """ Reorder helices by moving helices _pathHelixList[first:last] by a distance delta in the list. Notify each PathHelix and PathHelixHandle of its new location. Args: first (TYPE): Description last (TYPE): Description index_delta (TYPE): Description """ vhi_list = self._virtual_helix_item_list helix_numbers = [vhi.idNum() for vhi in vhi_list] first_index = helix_numbers.index(id_nums[0]) last_index = helix_numbers.index(id_nums[-1]) + 1 for id_num in id_nums: helix_numbers.remove(id_num) if index_delta < 0: # move group earlier in the list new_index = max(0, index_delta + first_index) - len(id_nums) else: # move group later in list new_index = min(len(vhi_list), index_delta + last_index) - len(id_nums) new_list = helix_numbers[:new_index] + id_nums + helix_numbers[ new_index:] # call the method to move the items and store the list self._model_part.setImportedVHelixOrder(new_list, check_batch=False) # end def def setActiveVirtualHelixItem(self, new_active_vhi): """Summary Args: new_active_vhi (TYPE): Description Returns: TYPE: Description """ current_vhi = self.active_virtual_helix_item if new_active_vhi != current_vhi: if current_vhi is not None: current_vhi.deactivate() if new_active_vhi is not None: new_active_vhi.activate() self.active_virtual_helix_item = new_active_vhi # end def def unsetActiveVirtualHelixItem(self): if self.active_virtual_helix_item is not None: self.active_virtual_helix_item.deactivate() self.active_virtual_helix_item = None self.prexover_manager.reset() def setPreXoverItemsVisible(self, virtual_helix_item): """ self._pre_xover_items list references prexovers parented to other PathHelices such that only the activeHelix maintains the list of visible prexovers Args: virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): Description """ vhi = virtual_helix_item if vhi is None: return # print("path.setPreXoverItemsVisible", virtual_helix_item.idNum()) part = self.part() info = part.active_base_info if info and virtual_helix_item is not None: id_num, is_fwd, idx, to_vh_id_num = info per_neighbor_hits, pairs = part.potentialCrossoverMap(id_num, idx) self.prexover_manager.activateVirtualHelix(virtual_helix_item, idx, per_neighbor_hits) else: self.prexover_manager.reset() # end def def updateXoverItems(self, virtual_helix_item): """Summary Args: virtual_helix_item (cadnano.views.pathview.virtualhelixitem.VirtualHelixItem): Description Returns: TYPE: Description """ for item in virtual_helix_item.childItems(): if isinstance(item, XoverNode3): item.refreshXover() # end def def updateStatusBar(self, status_string): """Shows status_string in the MainWindow's status bar. Args: status_string (str): The text to be displayed. """ self.window().statusBar().showMessage(status_string) ### COORDINATE METHODS ### def keyPanDeltaX(self): """How far a single press of the left or right arrow key should move the scene (in scene space) """ vhs = self._virtual_helix_item_list return vhs[0].keyPanDeltaX() if vhs else 5 # end def def keyPanDeltaY(self): """How far an an arrow key should move the scene (in scene space) for a single press """ vhs = self._virtual_helix_item_list if not len(vhs) > 1: return 5 dy = vhs[0].pos().y() - vhs[1].pos().y() dummyRect = QRectF(0, 0, 1, dy) return self.mapToScene(dummyRect).boundingRect().height() # end def ### TOOL METHODS ### def mousePressEvent(self, event): """Handler for user mouse press. Args: event (:obj:`QGraphicsSceneMouseEvent`): Contains item, scene, and screen coordinates of the the event, and previous event. """ self._viewroot.clearSelectionsIfActiveTool() self.unsetActiveVirtualHelixItem() return QGraphicsItem.mousePressEvent(self, event) def hoverMoveEvent(self, event): """ Parses a mouseMoveEvent to extract strandSet and base index, forwarding them to approproate tool method as necessary. Args: event (TYPE): Description """ active_tool = self._getActiveTool() tool_method_name = active_tool.methodPrefix() + "HoverMove" if hasattr(self, tool_method_name): getattr(self, tool_method_name)(event.pos()) # end def def createToolHoverMove(self, pt): """Create the strand is possible. Args: pt (QPointF): mouse cursor location of create tool hover. """ active_tool = self._getActiveTool() if not active_tool.isFloatingXoverBegin(): temp_xover = active_tool.floatingXover() temp_xover.updateFloatingFromPartItem(self, pt)