class ActiveSliceItem(QGraphicsRectItem): """ActiveSliceItem for the Path View""" def __init__(self, part_item, active_base_index): super(ActiveSliceItem, self).__init__(part_item) self._part_item = part_item self._getActiveTool = part_item._getActiveTool self._active_slice = 0 self._low_drag_bound = 0 self._high_drag_bound = self.part().maxBaseIdx() self._controller = ActiveSliceItemController(self, part_item.part()) self._label = QGraphicsSimpleTextItem("", parent=self) self._label.setPos(0, -18) self._label.setFont(_FONT) self._label.setBrush(_LABEL_BRUSH) self._label.hide() self.setFlag(QGraphicsItem.ItemIsMovable) self.setAcceptHoverEvents(True) self.setZValue(styles.ZACTIVESLICEHANDLE) self.setRect(QRectF(0, 0, _BASE_WIDTH,\ self._part_item.boundingRect().height())) self.setPos(active_base_index*_BASE_WIDTH, 0) self.setBrush(_BRUSH) self.setPen(_PEN) # reuse select tool methods for other tools self.addSeqToolMousePress = self.selectToolMousePress self.addSeqToolMouseMove = self.selectToolMouseMove self.breakToolMousePress = self.selectToolMousePress self.breakToolMouseMove = self.selectToolMouseMove self.insertionToolMousePress = self.selectToolMousePress self.insertionToolMouseMove = self.selectToolMouseMove self.paintToolMousePress = self.selectToolMousePress self.paintToolMouseMove = self.selectToolMouseMove self.pencilToolMousePress = self.selectToolMousePress self.pencilToolMouseMove = self.selectToolMouseMove self.skipToolMousePress = self.selectToolMousePress self.skipToolMouseMove = self.selectToolMouseMove # end def ### SLOTS ### def strandChangedSlot(self, sender, vh): pass # end def def updateRectSlot(self, part): bw = _BASE_WIDTH new_rect = QRectF(0, 0, bw,\ self._part_item.virtualHelixBoundingRect().height()) if new_rect != self.rect(): self.setRect(new_rect) self._hideIfEmptySelection() self.updateIndexSlot(part, part.activeBaseIndex()) return new_rect # end def def updateIndexSlot(self, part, base_index): """The slot that receives active slice changed notifications from the part and changes the receiver to reflect the part""" label = self._label bw = _BASE_WIDTH bi = util.clamp(int(base_index), 0, self.part().maxBaseIdx()) self.setPos(bi * bw, -styles.PATH_HELIX_PADDING) self._active_slice = bi if label: label.setText("%d" % bi) label.setX((bw - label.boundingRect().width()) / 2) # end def ### ACCESSORS ### def activeBaseIndex(self): return self.part().activeBaseIndex() # end def def part(self): return self._part_item.part() # end def def partItem(self): return self._part_item # end def ### PUBLIC METHODS FOR DRAWING / LAYOUT ### def removed(self): scene = self.scene() scene.removeItem(self._label) scene.removeItem(self) self._part_item = None self._label = None self._controller.disconnectSignals() self.controller = None # end def def resetBounds(self): """Call after resizing virtualhelix canvas.""" self._high_drag_bound = self.part().maxBaseIdx() # end def ### PRIVATE SUPPORT METHODS ### def _hideIfEmptySelection(self): vis = self.part().numberOfVirtualHelices() > 0 self.setVisible(vis) self._label.setVisible(vis) # end def def _setActiveBaseIndex(self, base_index): self.part().setActiveBaseIndex(base_index) # end def ### EVENT HANDLERS ### def hoverEnterEvent(self, event): self.setCursor(Qt.OpenHandCursor) self._part_item.updateStatusBar("%d" % self.part().activeBaseIndex()) QGraphicsItem.hoverEnterEvent(self, event) # end def def hoverLeaveEvent(self, event): self.setCursor(Qt.ArrowCursor) self._part_item.updateStatusBar("") QGraphicsItem.hoverLeaveEvent(self, event) # end def def mousePressEvent(self, event): """ Parses a mousePressEvent, calling the approproate tool method as necessary. Stores _move_idx for future comparison. """ if event.button() != Qt.LeftButton: event.ignore() QGraphicsItem.mousePressEvent(self, event) return self.scene().views()[0].addToPressList(self) self._move_idx = int(floor((self.x() + event.pos().x()) / _BASE_WIDTH)) tool_method_name = self._getActiveTool().methodPrefix() + "MousePress" if hasattr(self, tool_method_name): # if the tool method exists modifiers = event.modifiers() getattr(self, tool_method_name)(modifiers) # call tool method def mouseMoveEvent(self, event): """ Parses a mouseMoveEvent, calling the approproate tool method as necessary. Updates _move_idx if it changed. """ tool_method_name = self._getActiveTool().methodPrefix() + "MouseMove" if hasattr(self, tool_method_name): # if the tool method exists idx = int(floor((self.x() + event.pos().x()) / _BASE_WIDTH)) if idx != self._move_idx: # did we actually move? modifiers = event.modifiers() self._move_idx = idx getattr(self, tool_method_name)(modifiers, idx) # call tool method def customMouseRelease(self, event): """ Parses a mouseReleaseEvent, calling the approproate tool method as necessary. Deletes _move_idx if necessary. """ tool_method_name = self._getActiveTool().methodPrefix() + "MouseRelease" if hasattr(self, tool_method_name): # if the tool method exists modifiers = event.modifiers() x = event.pos().x() getattr(self, tool_method_name)(modifiers, x) # call tool method if hasattr(self, '_move_idx'): del self._move_idx ### TOOL METHODS ### def selectToolMousePress(self, modifiers): """ Set the allowed drag bounds for use by selectToolMouseMove. """ if (modifiers & Qt.AltModifier) and (modifiers & Qt.ShiftModifier): self.part().undoStack().beginMacro("Auto-drag Scaffold(s)") for vh in self.part().getVirtualHelices(): # SCAFFOLD # resize 3' first for strand in vh.scaffoldStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx3p): lo, hi = strand.getResizeBounds(idx3p) if strand.isDrawn5to3(): strand.resize((idx5p, hi)) else: strand.resize((lo, idx5p)) # resize 5' second for strand in vh.scaffoldStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx5p): lo, hi = strand.getResizeBounds(idx5p) if strand.isDrawn5to3(): strand.resize((lo, idx3p)) else: strand.resize((idx3p, hi)) # STAPLE # resize 3' first for strand in vh.stapleStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx3p): lo, hi = strand.getResizeBounds(idx3p) if strand.isDrawn5to3(): strand.resize((idx5p, hi)) else: strand.resize((lo, idx5p)) # resize 5' second for strand in vh.stapleStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx3p): lo, hi = strand.getResizeBounds(idx5p) if strand.isDrawn5to3(): strand.resize((lo, idx3p)) else: strand.resize((idx3p, hi)) self.part().undoStack().endMacro() # end def def selectToolMouseMove(self, modifiers, idx): """ Given a new index (pre-validated as different from the prev index), calculate the new x coordinate for self, move there, and notify the parent strandItem to redraw its horizontal line. """ idx = util.clamp(idx, self._low_drag_bound, self._high_drag_bound) x = int(idx * _BASE_WIDTH) self.setPos(x, self.y()) self.updateIndexSlot(None, idx) self._setActiveBaseIndex(idx) self._part_item.updateStatusBar("%d" % self.part().activeBaseIndex())
class EdgeItem(QGraphicsItem): LINE_WIDTH = 1 OFFSET = 8 # 方向线偏离中心线的距离 MIN_ARROW_WIDTH, MAX_ARROW_WIDTH = 1, 8 double_click_callback = EMPTY_FUNC def __init__(self, edge_id): super().__init__() self.setZValue(1) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) self.setAcceptHoverEvents(True) self.edge_id = edge_id self.text_item = QGraphicsSimpleTextItem('', self) self.text_item.setZValue(4) self.style = { 'name': f'Edge{edge_id}', 'color': Qt.black, 'width': 0.5, # 0~1 的中间值 'line': Qt.SolidLine, 'show_arrow': False, 'text': '', 'text_color': Qt.black, 'show_text': False, } self.hover = False def type(self): return QGraphicsItem.UserType + abs(hash(EdgeItem)) def boundingRect(self): return self.bounding_rect def shape(self): path = QPainterPath() path.addPolygon(self.shape_polygon) path.closeSubpath() return path # ------------------------------------------------------------------------- def adjust(self, src_p: QPointF, dst_p: QPointF): self.angle = getAngle(src_p, dst_p) self.src_p = src_p self.arrow_p = (src_p + 2 * dst_p) / 3 # 箭头开始位置, 前端2/3处 self.dst_p = dst_p W1 = 1 * self.OFFSET W2 = 2 * self.OFFSET W3 = 3 * self.OFFSET vec = getRightOffsetVector(self.angle) self.arrow_polygon = QPolygonF([ src_p + vec * W1, dst_p + vec * W1, self.arrow_p + vec * W2, self.arrow_p + vec * W1 ]) self.shape_polygon = QPolygonF( [src_p, src_p + vec * W2, dst_p + vec * W2, dst_p]) self.bounding_rect = QRectF(src_p, dst_p).normalized() # normalized 正方向 self.bounding_rect.adjust(-W3, -W3, W3, W3) self.text_p = ((src_p + dst_p) / 2) + vec * W1 self.text_item.setPos(self.text_p) self.prepareGeometryChange() # ------------------------------------------------------------------------- def paint(self, painter, option, widget=None): if self.style['show_arrow'] or self.hover: width = threshold(0.0, self.style['width'], 1.0) width = width * (self.MAX_ARROW_WIDTH - self.MIN_ARROW_WIDTH) + self.MIN_ARROW_WIDTH painter.setPen(QPen(self.style['color'], width, self.style['line'])) painter.setBrush(self.style['color']) painter.drawPolygon(self.arrow_polygon) else: # TODO 定制线类型 虚线或实线 painter.setPen(QPen(Qt.black, self.LINE_WIDTH)) painter.drawLine(self.src_p, self.dst_p) if (self.style['show_arrow'] and self.style['show_text']) or self.hover: self.text_item.setPen(self.style['text_color']) self.text_item.setText( f"{self.style['name']}\n{self.style['text']}") self.text_item.show() else: self.text_item.hide() # ------------------------------------------------------------------------- def mouseDoubleClickEvent(self, event): self.double_click_callback(self.edge_id) super().mouseDoubleClickEvent(event) def hoverEnterEvent(self, event): self.hover = True self.update() super().hoverEnterEvent(event) def hoverLeaveEvent(self, event): self.hover = False self.update() super().hoverLeaveEvent(event) # ------------------------------------------------------------------------- def setStyle(self, style) -> None: for key in self.style: try: self.style[key] = style[key] except KeyError: pass self.update()
class StickWidget(QGraphicsObject): font: QFont = QFont("monospace", 32) delete_clicked = pyqtSignal(Stick) link_initiated = pyqtSignal('PyQt_PyObject') # Actually StickWidget link_accepted = pyqtSignal('PyQt_PyObject') hovered = pyqtSignal(['PyQt_PyObject', 'PyQt_PyObject']) stick_changed = pyqtSignal('PyQt_PyObject') sibling_changed = pyqtSignal(bool) right_clicked = pyqtSignal('PyQt_PyObject') handle_idle_brush = QBrush(QColor(0, 125, 125, 50)) handle_hover_brush = QBrush(QColor(125, 125, 0, 50)) handle_press_brush = QBrush(QColor(200, 200, 0, 0)) handle_idle_pen = QPen(QColor(0, 0, 0, 255)) handle_press_pen = QPen(QColor(200, 200, 0, 255)) handle_size = 20 normal_color = QColor(0, 200, 120) negative_color = QColor(200, 0, 0) positive_color = QColor(0, 200, 0) mismatched = pyqtSignal('PyQt_PyObject') misplaced = pyqtSignal('PyQt_PyObject') measurement_corrected = pyqtSignal('PyQt_PyObject') clearly_visible = pyqtSignal('PyQt_PyObject') zero_clicked = pyqtSignal('PyQt_PyObject') def __init__(self, stick: Stick, camera: Camera, parent: Optional[QGraphicsItem] = None): QGraphicsObject.__init__(self, parent) self.camera = camera self.stick = stick self.line = QLineF() self.gline = QGraphicsLineItem(self.line) self.stick_label_text = QGraphicsSimpleTextItem("0", self) self.stick_label_text.setFont(StickWidget.font) self.stick_label_text.setPos(self.line.p1() - QPoint(0, 24)) self.stick_label_text.setBrush(QBrush(QColor(0, 255, 0))) self.stick_label_text.hide() self.setZValue(10) self.mode = StickMode.Display self.btn_delete = Button("delete", "x", parent=self) self.btn_delete.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) self.btn_delete.set_base_color([ButtonColor.RED]) self.btn_delete.setVisible(False) btn_size = max(int(np.linalg.norm(self.stick.top - self.stick.bottom) / 5.0), 15) self.btn_delete.set_height(12) self.btn_delete.clicked.connect(self.handle_btn_delete_clicked) self.btn_delete.setPos(self.line.p1() - QPointF(0.5 * self.btn_delete.boundingRect().width(), 1.1 * self.btn_delete.boundingRect().height())) self.btn_delete.set_opacity(0.7) self.top_handle = QGraphicsEllipseItem(0, 0, self.handle_size, self.handle_size, self) self.mid_handle = QGraphicsEllipseItem(0, 0, self.handle_size, self.handle_size, self) self.bottom_handle = QGraphicsEllipseItem(0, 0, self.handle_size, self.handle_size, self) self.top_handle.setAcceptedMouseButtons(Qt.NoButton) self.mid_handle.setAcceptedMouseButtons(Qt.NoButton) self.bottom_handle.setAcceptedMouseButtons(Qt.NoButton) self.top_handle.setBrush(self.handle_idle_brush) self.top_handle.setPen(self.handle_idle_pen) self.mid_handle.setBrush(self.handle_idle_brush) self.mid_handle.setPen(self.handle_idle_pen) self.bottom_handle.setBrush(self.handle_idle_brush) self.bottom_handle.setPen(self.handle_idle_pen) self.hovered_handle: Optional[QGraphicsRectItem] = None self.handles = [self.top_handle, self.mid_handle, self.bottom_handle] self.link_button = Button("link", "Link to...", parent=self) self.link_button.set_base_color([ButtonColor.GREEN]) self.link_button.set_height(12) self.link_button.set_label("Link", direction="vertical") self.link_button.fit_to_contents() self.link_button.clicked.connect(lambda: self.link_initiated.emit(self)) self.link_button.setVisible(False) self.link_button.setFlag(QGraphicsObject.ItemIgnoresTransformations, False) self.adjust_line() self.setAcceptHoverEvents(True) self.top_handle.setZValue(4) self.bottom_handle.setZValue(4) self.mid_handle.setZValue(4) self.top_handle.hide() self.mid_handle.hide() self.bottom_handle.hide() self.handle_mouse_offset = QPointF(0, 0) self.available_for_linking = False self.link_source = False self.current_highlight_color: QColor = StickWidget.normal_color self.highlighted = False self.frame_color: Optional[None] = self.normal_color self.is_linked = False self.is_master = True self.selected = False self.measured_height: int = -1 self.current_color = self.normal_color self.show_label = False self.highlight_animation = QPropertyAnimation(self, b"highlight_color") self.highlight_animation.valueChanged.connect(self.handle_highlight_animation_value_changed) self.deleting = False self.update_tooltip() self.show_measurements: bool = False self.proposed_snow_height: int = -1 self.zero_btn = Button("zero_btn", "0", parent=self) self.zero_btn.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) self.zero_btn.setVisible(False) self.zero_btn.setPos(self.boundingRect().center() + QPointF(self.zero_btn.boundingRect().width() * -0.5, self.boundingRect().height() * 0.5)) self.zero_btn.clicked.connect(self.handle_zero) @pyqtSlot() def handle_btn_delete_clicked(self): self.delete_clicked.emit(self.stick) def prepare_for_deleting(self): self.deleting = True self.highlight_animation.stop() self.btn_delete.setParentItem(None) self.scene().removeItem(self.btn_delete) self.btn_delete.deleteLater() def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: Optional[PyQt5.QtWidgets.QWidget] = ...): painter.setPen(QPen(self.current_color, 1.0)) brush = QBrush(self.current_highlight_color) pen = QPen(brush, 4) painter.setPen(pen) if self.highlighted: painter.fillRect(self.boundingRect(), QBrush(self.current_highlight_color)) if self.frame_color is not None and self.mode != StickMode.Edit and self.mode != StickMode.EditDelete: painter.setPen(QPen(self.frame_color, 4)) painter.drawRect(self.boundingRect()) pen = QPen(QColor(0, 255, 0, 255)) pen.setWidth(1.0) pen.setColor(QColor(255, 0, 255, 255)) pen.setStyle(Qt.DotLine) painter.setPen(pen) off = 10 painter.drawLine(self.line.p1() - QPointF(0, off), self.line.p1() + QPointF(0, off)) painter.drawLine(self.line.p1() - QPointF(off, 0), self.line.p1() + QPointF(off, 0)) painter.drawLine(self.line.p2() - QPointF(0, off), self.line.p2() + QPointF(0, off)) painter.drawLine(self.line.p2() - QPointF(off, 0), self.line.p2() + QPointF(off, 0)) pen.setStyle(Qt.SolidLine) pen.setColor(QColor(0, 255, 0, 255)) painter.setPen(pen) if self.mode != StickMode.EditDelete: pen.setWidth(2.0) br = painter.brush() painter.setPen(pen) painter.drawEllipse(self.line.p1(), 10, 10) painter.drawEllipse(self.line.p2(), 10, 10) painter.setBrush(br) if self.mode == StickMode.Measurement and self.proposed_snow_height >= 0: point = QPointF(self.boundingRect().x(), -self.proposed_snow_height + self.line.p2().y()) pen = QPen(QColor(200, 100, 0, 255), 3.0) painter.setPen(pen) painter.drawLine(point, point + QPointF(self.boundingRect().width(), 0.0)) if self.measured_height >= 0: vec = (self.stick.top - self.stick.bottom) / np.linalg.norm(self.stick.top - self.stick.bottom) dist_along_stick = self.measured_height / np.dot(np.array([0.0, -1.0]), vec) point = self.line.p2() + dist_along_stick * QPointF(vec[0], vec[1]) point = QPointF(self.boundingRect().x(), point.y()) pen = QPen(QColor(0, 100, 200, 255), 3.0) painter.setPen(pen) painter.drawLine(point, point + QPointF(self.boundingRect().width(), 0.0)) else: painter.drawLine(self.line.p1(), self.line.p2()) if self.selected: pen.setColor(QColor(255, 125, 0, 255)) pen.setStyle(Qt.DashLine) painter.setPen(pen) painter.drawRect(self.boundingRect().marginsAdded(QMarginsF(5, 5, 5, 5))) if self.show_measurements: painter.fillRect(self.stick_label_text.boundingRect().translated(self.stick_label_text.pos()), QBrush(QColor(0, 0, 0, 120))) def boundingRect(self) -> PyQt5.QtCore.QRectF: return self.gline.boundingRect().united(self.top_handle.boundingRect()).\ united(self.mid_handle.boundingRect()).united(self.bottom_handle.boundingRect()) def set_edit_mode(self, value: bool): if value: self.set_mode(StickMode.EditDelete) else: self.set_mode(StickMode.Display) def set_mode(self, mode: StickMode): if mode == StickMode.Display: self.btn_delete.setVisible(False) self.top_handle.setVisible(False) self.mid_handle.setVisible(False) self.bottom_handle.setVisible(False) self.link_button.setVisible(False) self.available_for_linking = False self.link_source = False self.zero_btn.setVisible(False) self.setVisible(self.stick.is_visible) elif mode == StickMode.EditDelete: self.set_mode(StickMode.Display) self.top_handle.setVisible(True) self.mid_handle.setVisible(True) self.bottom_handle.setVisible(True) self.available_for_linking = False self.link_source = False self.btn_delete.setVisible(True) elif mode == StickMode.LinkSource: self.set_mode(StickMode.Display) self.link_source = True self.available_for_linking = False self.link_button.setPos(self.boundingRect().topLeft()) self.link_button.set_width(int(self.boundingRect().width())) self.link_button.set_button_height(int(self.boundingRect().height())) self.link_button.adjust_text_to_button() elif mode == StickMode.LinkTarget: self.set_mode(StickMode.Display) self.link_source = False self.available_for_linking = True elif mode == StickMode.Edit: self.set_mode(StickMode.EditDelete) self.btn_delete.setVisible(False) elif mode == StickMode.Measurement: self.zero_btn.setVisible(True) self.setVisible(True) self.mode = mode self.update_tooltip() self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): if self.mode != StickMode.EditDelete: return if self.hovered_handle is None: return self.hovered_handle.setBrush(self.handle_press_brush) if self.hovered_handle == self.mid_handle: self.bottom_handle.setBrush(self.handle_press_brush) self.bottom_handle.setPen(self.handle_press_pen) self.bottom_handle.setOpacity(0.5) self.top_handle.setBrush(self.handle_press_brush) self.top_handle.setPen(self.handle_press_pen) self.top_handle.setOpacity(0.5) self.hovered_handle.setPen(self.handle_press_pen) self.hovered_handle.setOpacity(0.5) self.handle_mouse_offset = self.hovered_handle.rect().center() - event.pos() self.btn_delete.setVisible(False) def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): if self.available_for_linking: self.link_accepted.emit(self) return if self.mode == StickMode.Measurement: old_snow = self.stick.snow_height_px self.measured_height = self.proposed_snow_height self.stick.set_snow_height_px(self.proposed_snow_height) if abs(old_snow - self.proposed_snow_height) > 0: self.measurement_corrected.emit(self) self.proposed_snow_height = -1 if self.mode != StickMode.EditDelete and self.mode != StickMode.Edit: return if self.hovered_handle is not None: self.hovered_handle.setBrush(self.handle_hover_brush) self.hovered_handle.setPen(self.handle_idle_pen) self.hovered_handle.setOpacity(1.0) if self.hovered_handle == self.mid_handle: self.bottom_handle.setBrush(self.handle_idle_brush) self.bottom_handle.setPen(self.handle_idle_pen) self.bottom_handle.setOpacity(1.0) self.top_handle.setBrush(self.handle_idle_brush) self.top_handle.setPen(self.handle_idle_pen) self.top_handle.setOpacity(1.0) self.stick_changed.emit(self) self.hovered_handle = None if self.mode == StickMode.EditDelete: self.btn_delete.setVisible(True) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): if self.hovered_handle is None: return if self.hovered_handle == self.top_handle: self.line.setP1((event.pos() + self.handle_mouse_offset).toPoint()) elif self.hovered_handle == self.bottom_handle: self.line.setP2((event.pos() + self.handle_mouse_offset).toPoint()) else: displacement = event.pos() - event.lastPos() self.setPos(self.pos() + displacement) self.adjust_handles() self.adjust_stick() self.scene().update() def set_top(self, pos: QPoint): self.line.setP1(pos) self.adjust_handles() self.adjust_stick() self.scene().update() def set_bottom(self, pos: QPoint): self.line.setP2(pos) self.adjust_handles() self.adjust_stick() self.scene().update() def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): if self.available_for_linking: self.hovered.emit(True, self) elif self.link_source: self.link_button.setVisible(True) self.scene().update() def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): for h in self.handles: h.setBrush(self.handle_idle_brush) self.hovered_handle = None if self.available_for_linking: self.hovered.emit(False, self) self.link_button.setVisible(False) self.proposed_snow_height = -1 self.scene().update() def hoverMoveEvent(self, event: QGraphicsSceneHoverEvent): if self.mode != StickMode.EditDelete and self.mode != StickMode.Edit and self.mode != StickMode.Measurement: return if self.mode == StickMode.Measurement: self.proposed_snow_height = max(self.line.p2().y() - event.pos().y(), 0) self.update() return hovered_handle = list(filter(lambda h: h.rect().contains(event.pos()), self.handles)) if len(hovered_handle) == 0: if self.hovered_handle is not None: self.hovered_handle.setBrush(self.handle_idle_brush) self.hovered_handle = None return if self.hovered_handle is not None and self.hovered_handle != hovered_handle[0]: self.hovered_handle.setBrush(self.handle_idle_brush) self.hovered_handle = hovered_handle[0] if self.hovered_handle == self.top_handle: self.top_handle.setBrush(self.handle_hover_brush) elif self.hovered_handle == self.bottom_handle: self.bottom_handle.setBrush(self.handle_hover_brush) else: self.mid_handle.setBrush(self.handle_hover_brush) self.scene().update() def adjust_stick(self): self.stick.top[0] = self.pos().x() + self.line.p1().x() self.stick.top[1] = self.pos().y() + self.line.p1().y() self.stick.bottom[0] = self.pos().x() + self.line.p2().x() self.stick.bottom[1] = self.pos().y() + self.line.p2().y() def adjust_handles(self): if self.line.p1().y() > self.line.p2().y(): p1, p2 = self.line.p1(), self.line.p2() self.line.setP1(p2) self.line.setP2(p1) if self.hovered_handle is not None: self.hovered_handle.setBrush(self.handle_idle_brush) self.hovered_handle.setPen(self.handle_idle_pen) self.hovered_handle = self.top_handle if self.hovered_handle == self.bottom_handle else self.bottom_handle self.hovered_handle.setBrush(self.handle_press_brush) self.hovered_handle.setPen(self.handle_press_pen) rect = self.top_handle.rect() rect.moveCenter(self.line.p1()) self.top_handle.setRect(rect) rect = self.bottom_handle.rect() rect.moveCenter(self.line.p2()) self.bottom_handle.setRect(rect) rect = self.mid_handle.rect() rect.moveCenter(self.line.center()) self.mid_handle.setRect(rect) self.btn_delete.setPos(self.top_handle.rect().center() - QPointF(self.btn_delete.boundingRect().width() / 2, self.btn_delete.boundingRect().height() + self.top_handle.boundingRect().height() / 2)) def set_available_for_linking(self, available: bool): self.available_for_linking = available def set_is_link_source(self, is_source: bool): self.link_source = is_source self.link_button.setPos(self.boundingRect().topLeft()) self.link_button.set_width(int(self.boundingRect().width())) self.link_button.set_button_height(int(self.boundingRect().height())) self.link_button.adjust_text_to_button() def set_frame_color(self, color: Optional[QColor]): self.frame_color = color if color is not None else self.normal_color self.update() def set_is_linked(self, value: bool): self.is_linked = value if not self.is_linked: self.set_frame_color(None) if self.available_for_linking: self.highlight(QColor(0, 255, 0, 100)) else: self.highlight(None) self.update_tooltip() def adjust_line(self): self.setPos(QPointF(0.5 * (self.stick.top[0] + self.stick.bottom[0]), 0.5 * (self.stick.top[1] + self.stick.bottom[1]))) vec = 0.5 * (self.stick.top - self.stick.bottom) self.line.setP1(QPointF(vec[0], vec[1])) self.line.setP2(-self.line.p1()) self.gline.setLine(self.line) self.adjust_handles() self.stick_label_text.setPos(self.line.p1() - QPointF(0.5 * self.stick_label_text.boundingRect().width(), 1.3 * self.stick_label_text.boundingRect().height())) self.update() def set_selected(self, selected: bool): self.selected = selected self.update() def is_selected(self) -> bool: return self.selected def set_snow_height(self, height: int): self.measured_height = height self.update() def border_normal(self): self.current_color = self.normal_color self.update() def border_positive(self): self.current_color = self.positive_color self.update() def border_negative(self): self.current_color = self.negative_color self.update() @pyqtProperty(QColor) def highlight_color(self) -> QColor: return self.current_highlight_color @highlight_color.setter def highlight_color(self, color: QColor): self.current_highlight_color = color def highlight(self, color: Optional[QColor], animated: bool = False): self.highlighted = color is not None if not animated or color is None: self.highlight_animation.stop() self.current_highlight_color = self.normal_color if color is None else color self.update() return self.highlight_animation.setStartValue(color) self.highlight_animation.setEndValue(color) self.highlight_animation.setKeyValueAt(0.5, color.darker()) self.highlight_animation.setDuration(2000) self.highlight_animation.setLoopCount(-1) self.highlight_animation.start() def handle_link_button_hovered(self, btn: Dict[str, Any]): self.link_button.setVisible(btn['hovered']) def handle_highlight_animation_value_changed(self, new: QColor): if not self.deleting: self.update(self.boundingRect().marginsAdded(QMarginsF(10, 10, 10, 10))) def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: self.right_clicked.emit({'stick_widget': self}) def set_stick_label(self, label: str): self.stick.label = label self.stick_label_text.setText(label) self.update_tooltip() self.update() def get_stick_label(self) -> str: return self.stick.label def get_stick_length_cm(self) -> int: return self.stick.length_cm def set_stick_length_cm(self, length: int): self.stick.length_cm = length self.update_tooltip() self.update() def update_tooltip(self): if self.mode != StickMode.Display or self.mode == StickMode.Measurement: self.setToolTip("") return snow_txt = "Snow height: " if self.stick.snow_height_px >= 0: snow_txt += str(self.stick.snow_height_cm) + " cm" self.stick_label_text.setText(str(self.stick.snow_height_cm)) else: snow_txt = "not measured" self.stick_label_text.setVisible(False) self.stick_label_text.setText(self.stick.label) self.stick_label_text.setVisible(True) stick_view_text = '' role = '' if self.stick.alternative_view is not None: alt_view = self.stick.alternative_view role = " - primary" alt = "Secondary" if not self.stick.primary: role = " - secondary" alt = "Primary" stick_view_text = f'\n{alt} view: {alt_view.label} in {alt_view.camera_folder.name}\n' mark = '*' if self.stick.determines_quality else '' self.setToolTip(f'{mark}{self.stick.label}{role}{stick_view_text}\nLength: {self.stick.length_cm} cm\n{snow_txt}') def set_stick(self, stick: Stick): self.reset_d_btns() self.stick = stick self.adjust_line() self.adjust_handles() self.set_snow_height(stick.snow_height_px) self.update_tooltip() self.set_show_measurements(self.show_measurements) if self.mode == StickMode.Measurement: self.set_frame_color(QColor(200, 100, 0, 100) if not self.stick.is_visible else None) self.setVisible(True) self.clearly_visible_btn.setVisible(not self.stick.is_visible) else: self.setVisible(self.stick.is_visible) def set_show_measurements(self, show: bool): self.show_measurements = show if self.show_measurements: self.stick_label_text.setText(str(self.stick.snow_height_cm) if self.stick.snow_height_cm >= 0 else "n/a") else: self.stick_label_text.setText(self.stick.label) self.update() def handle_zero(self): self.measured_height = 0 self.stick.set_snow_height_px(0) self.measurement_corrected.emit(self) def reset_d_btns(self): self.zero_btn.set_default_state()
class ActiveSliceItem(QGraphicsRectItem): """ActiveSliceItem for the Path View""" def __init__(self, part_item, active_base_index): super(ActiveSliceItem, self).__init__(part_item) self._part_item = part_item self._getActiveTool = part_item._getActiveTool self._active_slice = 0 self._low_drag_bound = 0 self._high_drag_bound = self.part().maxBaseIdx() self._controller = ActiveSliceItemController(self, part_item.part()) self._label = QGraphicsSimpleTextItem("", parent=self) self._label.setPos(0, -18) self._label.setFont(_FONT) self._label.setBrush(_LABEL_BRUSH) self._label.hide() self.setFlag(QGraphicsItem.ItemIsMovable) self.setAcceptHoverEvents(True) self.setZValue(styles.ZACTIVESLICEHANDLE) self.setRect(QRectF(0, 0, _BASE_WIDTH,\ self._part_item.boundingRect().height())) self.setPos(active_base_index * _BASE_WIDTH, 0) self.setBrush(_BRUSH) self.setPen(_PEN) # reuse select tool methods for other tools self.addSeqToolMousePress = self.selectToolMousePress self.addSeqToolMouseMove = self.selectToolMouseMove self.breakToolMousePress = self.selectToolMousePress self.breakToolMouseMove = self.selectToolMouseMove self.insertionToolMousePress = self.selectToolMousePress self.insertionToolMouseMove = self.selectToolMouseMove self.paintToolMousePress = self.selectToolMousePress self.paintToolMouseMove = self.selectToolMouseMove self.pencilToolMousePress = self.selectToolMousePress self.pencilToolMouseMove = self.selectToolMouseMove self.skipToolMousePress = self.selectToolMousePress self.skipToolMouseMove = self.selectToolMouseMove # end def ### SLOTS ### def strandChangedSlot(self, sender, vh): pass # end def def updateRectSlot(self, part): bw = _BASE_WIDTH new_rect = QRectF(0, 0, bw,\ self._part_item.virtualHelixBoundingRect().height()) if new_rect != self.rect(): self.setRect(new_rect) self._hideIfEmptySelection() self.updateIndexSlot(part, part.activeBaseIndex()) return new_rect # end def def updateIndexSlot(self, part, base_index): """The slot that receives active slice changed notifications from the part and changes the receiver to reflect the part""" label = self._label bw = _BASE_WIDTH bi = util.clamp(int(base_index), 0, self.part().maxBaseIdx()) self.setPos(bi * bw, -styles.PATH_HELIX_PADDING) self._active_slice = bi if label: label.setText("%d" % bi) label.setX((bw - label.boundingRect().width()) / 2) # end def ### ACCESSORS ### def activeBaseIndex(self): return self.part().activeBaseIndex() # end def def part(self): return self._part_item.part() # end def def partItem(self): return self._part_item # end def ### PUBLIC METHODS FOR DRAWING / LAYOUT ### def removed(self): scene = self.scene() scene.removeItem(self._label) scene.removeItem(self) self._part_item = None self._label = None self._controller.disconnectSignals() self.controller = None # end def def resetBounds(self): """Call after resizing virtualhelix canvas.""" self._high_drag_bound = self.part().maxBaseIdx() # end def ### PRIVATE SUPPORT METHODS ### def _hideIfEmptySelection(self): vis = self.part().numberOfVirtualHelices() > 0 self.setVisible(vis) self._label.setVisible(vis) # end def def _setActiveBaseIndex(self, base_index): self.part().setActiveBaseIndex(base_index) # end def ### EVENT HANDLERS ### def hoverEnterEvent(self, event): self.setCursor(Qt.OpenHandCursor) self._part_item.updateStatusBar("%d" % self.part().activeBaseIndex()) QGraphicsItem.hoverEnterEvent(self, event) # end def def hoverLeaveEvent(self, event): self.setCursor(Qt.ArrowCursor) self._part_item.updateStatusBar("") QGraphicsItem.hoverLeaveEvent(self, event) # end def def mousePressEvent(self, event): """ Parses a mousePressEvent, calling the approproate tool method as necessary. Stores _move_idx for future comparison. """ if event.button() != Qt.LeftButton: event.ignore() QGraphicsItem.mousePressEvent(self, event) return self.scene().views()[0].addToPressList(self) self._move_idx = int(floor((self.x() + event.pos().x()) / _BASE_WIDTH)) tool_method_name = self._getActiveTool().methodPrefix() + "MousePress" if hasattr(self, tool_method_name): # if the tool method exists modifiers = event.modifiers() getattr(self, tool_method_name)(modifiers) # call tool method def mouseMoveEvent(self, event): """ Parses a mouseMoveEvent, calling the approproate tool method as necessary. Updates _move_idx if it changed. """ tool_method_name = self._getActiveTool().methodPrefix() + "MouseMove" if hasattr(self, tool_method_name): # if the tool method exists idx = int(floor((self.x() + event.pos().x()) / _BASE_WIDTH)) if idx != self._move_idx: # did we actually move? modifiers = event.modifiers() self._move_idx = idx getattr(self, tool_method_name)(modifiers, idx) # call tool method def customMouseRelease(self, event): """ Parses a mouseReleaseEvent, calling the approproate tool method as necessary. Deletes _move_idx if necessary. """ tool_method_name = self._getActiveTool().methodPrefix( ) + "MouseRelease" if hasattr(self, tool_method_name): # if the tool method exists modifiers = event.modifiers() x = event.pos().x() getattr(self, tool_method_name)(modifiers, x) # call tool method if hasattr(self, '_move_idx'): del self._move_idx ### TOOL METHODS ### def selectToolMousePress(self, modifiers): """ Set the allowed drag bounds for use by selectToolMouseMove. """ if (modifiers & Qt.AltModifier) and (modifiers & Qt.ShiftModifier): self.part().undoStack().beginMacro("Auto-drag Scaffold(s)") for vh in self.part().getVirtualHelices(): # SCAFFOLD # resize 3' first for strand in vh.scaffoldStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx3p): lo, hi = strand.getResizeBounds(idx3p) if strand.isDrawn5to3(): strand.resize((idx5p, hi)) else: strand.resize((lo, idx5p)) # resize 5' second for strand in vh.scaffoldStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx5p): lo, hi = strand.getResizeBounds(idx5p) if strand.isDrawn5to3(): strand.resize((lo, idx3p)) else: strand.resize((idx3p, hi)) # STAPLE # resize 3' first for strand in vh.stapleStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx3p): lo, hi = strand.getResizeBounds(idx3p) if strand.isDrawn5to3(): strand.resize((idx5p, hi)) else: strand.resize((lo, idx5p)) # resize 5' second for strand in vh.stapleStrandSet(): idx5p = strand.idx5Prime() idx3p = strand.idx3Prime() if not strand.hasXoverAt(idx3p): lo, hi = strand.getResizeBounds(idx5p) if strand.isDrawn5to3(): strand.resize((lo, idx3p)) else: strand.resize((idx3p, hi)) self.part().undoStack().endMacro() # end def def selectToolMouseMove(self, modifiers, idx): """ Given a new index (pre-validated as different from the prev index), calculate the new x coordinate for self, move there, and notify the parent strandItem to redraw its horizontal line. """ idx = util.clamp(idx, self._low_drag_bound, self._high_drag_bound) x = int(idx * _BASE_WIDTH) self.setPos(x, self.y()) self.updateIndexSlot(None, idx) self._setActiveBaseIndex(idx) self._part_item.updateStatusBar("%d" % self.part().activeBaseIndex())