def __init__(self, model_part, parent):
        super(OutlineNucleicAcidPartItem, self).__init__(model_part, parent)
        self._controller = NucleicAcidPartItemController(self, model_part)
        self._model_part = model_part
        self.setExpanded(True)
        # properties
        temp_color = model_part.getColor()
        # outlinerview takes responsibility of overriding default part color
        if temp_color == "#000000":
            index = len(model_part.document().children()) - 1
            new_color = styles.PARTCOLORS[index % len(styles.PARTCOLORS)]
            model_part.setProperty('color', new_color)

        # item groups
        self._root_items = {}
        self._root_items['VHelixList'] = self.createRootPartItem(
            'Virtual Helices', self)
        self._root_items['OligoList'] = self.createRootPartItem('Oligos', self)
        fs = model_part.document().filter_set
        if OutlineVirtualHelixItem.FILTER_NAME in fs:
            self._root_items['OligoList'].setFlags(DISABLE_FLAGS)
        else:
            self._root_items['VHelixList'].setFlags(DISABLE_FLAGS)
        # self._root_items['Modifications'] = self._createRootItem('Modifications', self)
        if model_part.is_active:
            print("should be active")
            self.activate()
    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
Exemple #3
0
    def __init__(self, model_part_instance, viewroot, parent=None):
        """Summary

        Args:
            model_part_instance (TYPE): Description
            viewroot (TYPE): Description
            parent (None, optional): Description
        """
        super(GridNucleicAcidPartItem, self).__init__(model_part_instance, viewroot, parent)

        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.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)

        # Cache of VHs that were active as of last call to activeGridChanged
        # If None, all grids 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))

        GC_SIZE = 10
        self.grab_cornerTL = GrabCornerItem(GC_SIZE, model_color, True, self)
        self.grab_cornerTL.setTopLeft(o_rect.topLeft())
        self.grab_cornerBR = GrabCornerItem(GC_SIZE, model_color, True, self)
        self.grab_cornerBR.setBottomRight(o_rect.bottomRight())
        self.griditem = GridItem(self, self._model_props['grid_type'])
        self.griditem.setZValue(1)
        self.grab_cornerTL.setZValue(2)
        self.grab_cornerBR.setZValue(2)

        # select upon creation
        for part in m_p.document().children():
            if part is m_p:
                part.setSelected(True)
            else:
                part.setSelected(False)
        self.show()
    def __init__(self, **kwargs):
        """Summary

        Args:
            model_part (Part): The model part
            parent (PropertyEditorWidget): The property editor
            key (None, optional): Description
        """
        super().__init__(**kwargs)
        if self._key == "name":
            for model_part in self.cnModelList():
                self._controller_list.append(NucleicAcidPartItemController(self, model_part))
    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()
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 OutlineNucleicAcidPartItem(CNOutlinerItem, AbstractPartItem):
    FILTER_NAME = "part"

    def __init__(self, model_part, parent):
        super(OutlineNucleicAcidPartItem, self).__init__(model_part, parent)
        self._controller = NucleicAcidPartItemController(self, model_part)
        self._model_part = model_part
        self.setExpanded(True)
        # properties
        temp_color = model_part.getColor()
        # outlinerview takes responsibility of overriding default part color
        if temp_color == "#000000":
            index = len(model_part.document().children()) - 1
            new_color = styles.PARTCOLORS[index % len(styles.PARTCOLORS)]
            model_part.setProperty('color', new_color)

        # item groups
        self._root_items = {}
        self._root_items['VHelixList'] = self.createRootPartItem(
            'Virtual Helices', self)
        self._root_items['OligoList'] = self.createRootPartItem('Oligos', self)
        fs = model_part.document().filter_set
        if OutlineVirtualHelixItem.FILTER_NAME in fs:
            self._root_items['OligoList'].setFlags(DISABLE_FLAGS)
        else:
            self._root_items['VHelixList'].setFlags(DISABLE_FLAGS)
        # self._root_items['Modifications'] = self._createRootItem('Modifications', self)
        if model_part.is_active:
            print("should be active")
            self.activate()

    # end def

    ### PRIVATE SUPPORT METHODS ###
    def __repr__(self):
        return "OutlineNucleicAcidPartItem %s" % self._cn_model.getProperty(
            'name')

    ### PUBLIC SUPPORT METHODS ###
    def rootItems(self):
        return self._root_items

    # end def

    def part(self):
        return self._cn_model

    # end def

    def itemType(self):
        return ItemType.NUCLEICACID

    # end def

    def isModelSelected(self, document):
        """Make sure the item is selected in the model
        TODO implement Part selection

        Args:
            document (Document): reference the the model :class:`Document`
        """
        return False

    # end def

    ### SLOTS ###
    def partRemovedSlot(self, sender):
        self._controller.disconnectSignals()
        self._cn_model = None
        self._controller = None

    # end def

    def partOligoAddedSlot(self, model_part, model_oligo):
        m_o = model_oligo
        m_o.oligoRemovedSignal.connect(self.partOligoRemovedSlot)
        o_i = OutlineOligoItem(m_o, self._root_items['OligoList'])
        self._oligo_item_hash[m_o] = o_i

    # end def

    def partOligoRemovedSlot(self, model_part, model_oligo):
        m_o = model_oligo
        m_o.oligoRemovedSignal.disconnect(self.partOligoRemovedSlot)
        o_i = self._oligo_item_hash[m_o]
        o_i.parent().removeChild(o_i)
        del self._oligo_item_hash[m_o]

    # end def

    def partVirtualHelixAddedSlot(self, model_part, id_num, virtual_helix,
                                  neighbors):
        tw = self.treeWidget()
        tw.is_child_adding += 1
        vh_i = OutlineVirtualHelixItem(virtual_helix,
                                       self._root_items['VHelixList'])
        self._virtual_helix_item_hash[id_num] = vh_i
        tw.is_child_adding -= 1

    def partVirtualHelixRemovingSlot(self, model_part, id_num, virtual_helix,
                                     neigbors):
        vh_i = self._virtual_helix_item_hash.get(id_num)
        # in case a OutlineVirtualHelixItem Object is cleaned up before this happends
        if vh_i is not None:
            del self._virtual_helix_item_hash[id_num]
            vh_i.parent().removeChild(vh_i)

    # end def

    def partPropertyChangedSlot(self, model_part, property_key, new_value):
        if self._cn_model == model_part:
            self.setValue(property_key, new_value)
            if property_key == 'virtual_helix_order':
                vhi_dict = self._virtual_helix_item_hash
                self.treeWidget().document()
                new_list = [vhi_dict[id_num] for id_num in new_value]
                # 0. record what was selected
                selected_list = [(x, x.isSelected()) for x in new_list]
                root_vhi = self._root_items['VHelixList']
                # 1. move the items
                root_vhi.takeChildren()
                for vhi in new_list:
                    root_vhi.addChild(vhi)
                # 2. now reselect the previously selected.
                # could also query the model
                for vhi, was_selected in selected_list:
                    if was_selected:
                        vhi.setSelected(True)

    # end def

    def partSelectedChangedSlot(self, model_part, is_selected):
        # print("part", is_selected)
        self.setSelected(is_selected)

    # end def

    def partVirtualHelixPropertyChangedSlot(self, sender, id_num,
                                            virtual_helix, keys, values):
        if self._cn_model == sender:
            vh_i = self._virtual_helix_item_hash[id_num]
            for key, val in zip(keys, values):
                if key in CNOutlinerItem.PROPERTIES:
                    vh_i.setValue(key, val)

    # end def

    def partVirtualHelicesSelectedSlot(self, sender, vh_set, is_adding):
        """ is_adding (bool): adding (True) virtual helices to a selection
        or removing (False)
        """
        vhi_hash = self._virtual_helix_item_hash
        tw = self.treeWidget()
        model = tw.model()
        selection_model = tw.selectionModel()
        top_idx = tw.indexOfTopLevelItem(self)
        top_midx = model.index(top_idx, 0)
        vh_list = self._root_items['VHelixList']
        root_midx = model.index(self.indexOfChild(vh_list), 0, top_midx)
        tw.selection_filter_disabled = True
        if is_adding:
            flag = QItemSelectionModel.Select
            for id_num in vh_set:
                vhi = vhi_hash.get(id_num)
                # selecting a selected item will deselect it, so check
                idx = vh_list.indexOfChild(vhi)
                qmodel_idx = model.index(idx, 0, root_midx)
                if not vhi.isSelected() and not selection_model.isSelected(
                        qmodel_idx):
                    # print("++++++slot Sselect outlinerview", vh_set)
                    selection_model.select(qmodel_idx, flag)
        else:
            flag = QItemSelectionModel.Deselect
            for id_num in vh_set:
                vhi = vhi_hash.get(id_num)
                # deselecting a deselected item will select it, so check
                idx = vh_list.indexOfChild(vhi)
                qmodel_idx = model.index(idx, 0, root_midx)
                if vhi.isSelected() and selection_model.isSelected(qmodel_idx):
                    # print("-----slot deselect outlinerview", vh_set)
                    selection_model.select(qmodel_idx, flag)
        tw.selection_filter_disabled = False

    # end def

    def partActiveVirtualHelixChangedSlot(self, part, id_num):
        vhi = self._virtual_helix_item_hash.get(id_num, None)
        # if vhi is not None:
        self.setActiveVirtualHelixItem(vhi)

    # end def

    def partActiveChangedSlot(self, part, is_active):
        if part == self._cn_model:
            self.activate() if is_active else self.deactivate()

    # end def

    def setActiveVirtualHelixItem(self, new_active_vhi):
        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
Exemple #8
0
class GridNucleicAcidPartItem(QAbstractPartItem):
    """Parent should be either a GridRootItem, 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.gridview.virtualhelixitem.GridVirtualHelixItem): Description
        grab_cornerBR (TYPE): bottom right bounding box handle
        grab_cornerTL (TYPE): top left bounding box handle
        griditem (TYPE): Description
        outline (TYPE): Description
        prexover_manager (TYPE): Description
        scale_factor (TYPE): Description
    """
    _RADIUS = styles.GRID_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(GridNucleicAcidPartItem, self).__init__(model_part_instance, viewroot, parent)

        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.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)

        # Cache of VHs that were active as of last call to activeGridChanged
        # If None, all grids 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))

        GC_SIZE = 10
        self.grab_cornerTL = GrabCornerItem(GC_SIZE, model_color, True, self)
        self.grab_cornerTL.setTopLeft(o_rect.topLeft())
        self.grab_cornerBR = GrabCornerItem(GC_SIZE, model_color, True, self)
        self.grab_cornerBR.setBottomRight(o_rect.bottomRight())
        self.griditem = GridItem(self, self._model_props['grid_type'])
        self.griditem.setZValue(1)
        self.grab_cornerTL.setZValue(2)
        self.grab_cornerBR.setZValue(2)

        # 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 (TYPE): Description
            id_num (int): VirtualHelix ID number. See `NucleicAcidPart` for description and related methods.

        Args:
            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

        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.grab_cornerTL.setPen(getPenObj(new_value, 0))
                self.grab_cornerBR.setPen(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.grab_cornerTL = None
        self.grab_cornerBR = None
        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.gridview.virtualhelixitem.GridVirtualHelixItem): 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 = GridVirtualHelixItem(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()
    # 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)
    # 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:
            # print("got the adding slot in path")
            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':
            print("grid change", value)
            if value == 'lines and points':
                self.griditem.setGridAppearance(True)
            elif value == 'points':
                self.griditem.setGridAppearance(False)
            elif value == 'circles':
                pass  # self.griditem.setGridAppearance(False)
            else:
                raise ValueError("unknown grid styling")
    # end def

    ### 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
        # print(current_vhi, new_active_vhi)
        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.gridview.virtualhelixitem.GridVirtualHelixItem): Description
        """
        vhi = virtual_helix_item
        pxom = self.prexover_manager
        if vhi is None:
            pxom.hideGroups()
            return

        # print("grid.setPreXoverItemsVisible", virtual_helix_item.idNum())
        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]
    # end def

    def reconfigureRect(self, top_left, bottom_right, finish=False, padding=80):
        """Summary

        Args:
            top_left (TYPE): Description
            bottom_right (TYPE): Description

        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 = new_rect = QRectF(ptTL, ptBR)
        self.setRect(new_rect)
        self._configureOutline(self.outline)
        self.griditem.updateGrid()
        return (ptTL.x(), ptTL.y()), (ptBR.x(), ptBR.y())
    # 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.  Call this
        when adding a GridVirtualHelixItem.
        """
        p = self._BOUNDING_RECT_PADDING
        xTL, yTL, xBR, yBR = self.getModelMinBounds()
        xTL = xTL - p
        yTL = yTL - p
        xBR = xBR + p
        yBR = yBR + p
        tl, br = self.reconfigureRect((xTL, yTL), (xBR, yBR))
        self.grab_cornerTL.alignPos(*tl)
        self.grab_cornerBR.alignPos(*br)
    # end def

    ### 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 boundaries to what's in the model with a minimum
        size
        """
        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)
        """
        xLL, yLL, xUR, yUR = self.part().boundDimensions(self.scale_factor)
        return xLL, -yUR, xUR, -yLL
    # 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 setModifyState(self, bool_val):
        """Hides the mod_rect when modify state disabled.

        Args:
            bool_val (TYPE): 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 updateStatusBar(self, status_str):
        """Shows status_str in the MainWindow's status bar.

        Args:
            status_str (TYPE): Description
        """
        pass  # disabled for now.
        # self.window().statusBar().showMessage(status_str, timeout)
    # end def

    def zoomToFit(self):
        """Summary

        Args:
            TYPE: Description
        """
        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:
            TYPE: Description
        """
        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 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):
        """Summary

        Args:
            event (TYPE): Description

        Args:
            TYPE: Description
        """

        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)
    # end def

    def hoverLeaveEvent(self, event):
        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 createToolMousePress(self, tool, event, alt_event=None):
        """Summary

        Args:
            tool (TYPE): Description
            event (TYPE): Description
            alt_event (None, optional): Description

        Returns:
            TYPE: Description
        """
        # 1. get point in model coordinates:
        part = self._model_part
        if alt_event is None:
            # print()
            pt = tool.eventToPosition(self, event)
            # print("reg_event", pt)
        else:
            # pt = alt_event.scenePos()
            # pt = self.mapFromScene(pt)
            pt = alt_event.pos()
            # print("alt_event", pt)

        if pt is None:
            tool.deactivate()
            return QGraphicsItem.mousePressEvent(self, event)

        part_pt_tuple = self.getModelPos(pt)

        mod = Qt.MetaModifier
        if not (event.modifiers() & mod):
            pass

        # don't create a new VirtualHelix if the click overlaps with existing
        # VirtualHelix
        current_id_num = tool.idNum()
        check = part.isVirtualHelixNearPoint(part_pt_tuple, current_id_num)
        # print("current_id_num", current_id_num, check)
        # print(part_pt_tuple)
        tool.setPartItem(self)
        if check:
            id_num = part.getVirtualHelixAtPoint(part_pt_tuple)
            # print("got a check", id_num)
            if id_num is not None:
                # print("restart", id_num)
                vhi = self._virtual_helix_item_hash[id_num]
                tool.setVirtualHelixItem(vhi)
                tool.startCreation()
        else:
            # print("creating", part_pt_tuple)
            part.createVirtualHelix(*part_pt_tuple)
            id_num = part.getVirtualHelixAtPoint(part_pt_tuple)
            vhi = self._virtual_helix_item_hash[id_num]
            tool.setVirtualHelixItem(vhi)
            tool.startCreation()
    # end def

    def createToolHoverMove(self, tool, event):
        """Summary

        Args:
            tool (TYPE): Description
            event (TYPE): Description

        Returns:
            TYPE: Description
        """
        tool.hoverMoveEvent(self, event)
        return QGraphicsItem.hoverMoveEvent(self, event)
    # end def

    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:
                print(id_num)
                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)
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)