class CartPoleShape: def __init__(self): self._boundingBox = QRectF(-239 / 2, -216 / 2, 239, 216) self._cartBody = QRectF(self._boundingBox.left(), self._boundingBox.top() + 82, self._boundingBox.width(), 106) self._cartJointBody = QRectF( self._cartBody.center().x() - 53 / 2, self._boundingBox.top() + 53 / 2, 53, self._cartBody.top() - self._boundingBox.top() - 53 / 2) self._cartJointBall = QRectF( self._cartJointBody.center().x() - self._cartJointBody.width() / 2, self._cartJointBody.top() - self._cartJointBody.width() / 2, self._cartJointBody.width(), self._cartJointBody.width()) self._poleCenter = self._cartJointBall.center() self._leftGear = QRectF(self._boundingBox.left() + 43 - 26, self._boundingBox.top() + 188 - 26, 26 * 2, 26 * 2) self._leftGearJoint = QRectF(self._boundingBox.left() + 43 - 2, self._boundingBox.top() + 188 - 2, 4, 4) self._rightGear = QRectF(self._boundingBox.left() + 194 - 26, self._boundingBox.top() + 188 - 26, 26 * 2, 26 * 2) self._rightGearJoint = QRectF(self._boundingBox.left() + 194 - 2, self._boundingBox.top() + 188 - 2, 4, 4) def draw(self, qp, pen): qp.setPen(pen) qp.drawRect(self._cartBody) qp.drawLines([ QLineF(self._cartJointBody.topLeft(), self._cartJointBody.bottomLeft()), QLineF(self._cartJointBody.topRight(), self._cartJointBody.bottomRight()), QLineF(self._cartJointBody.bottomLeft(), self._cartJointBody.bottomRight()) ]) qp.drawArc(self._cartJointBall, 0, 180 * 16) qp.drawEllipse(self._leftGear) qp.drawEllipse(self._leftGearJoint) qp.drawEllipse(self._rightGear) qp.drawEllipse(self._rightGearJoint) # Computes the model matrix that moves the cart in center and has # size width def modelMatrix(self, center, width): return QTransform.fromScale(width / 239, width / 239) * QTransform.fromTranslate( -center.x(), -center.y())
def _hitTest(self, rc: QRectF, mousePos: QPointF) -> Hit: maxdist = 4 if not rc.adjusted(-maxdist, -maxdist, maxdist, maxdist).contains(mousePos): return Hit.NoHit def dist(p1, p2): return (p1 - p2).manhattanLength() if dist(rc.topLeft(), mousePos) < maxdist: return Hit.TopLeft elif dist(rc.topRight(), mousePos) < maxdist: return Hit.TopRight elif dist(rc.bottomRight(), mousePos) < maxdist: return Hit.BottomRight elif dist(rc.bottomLeft(), mousePos) < maxdist: return Hit.BottomLeft elif abs(rc.left() - mousePos.x()) < maxdist: return Hit.Left elif abs(rc.right() - mousePos.x()) < maxdist: return Hit.Right elif abs(rc.top() - mousePos.y()) < maxdist: return Hit.Top elif abs(rc.bottom() - mousePos.y()) < maxdist: return Hit.Bottom elif rc.contains(mousePos): return Hit.Center else: return Hit.NoHit
def draw_bg(self, qp, rect, text): path = QPainterPath() # add container path.addRoundedRect(rect, 4, 4) if self.isEnabled(): highlight_color, bg_color, text_color = ( self.settings['highlight'], self.settings['bg'], self.settings['text']) else: highlight_color, bg_color, text_color = ( self.settings['highlight_disabled'], self.settings['bg_disabled'], self.settings['text_disabled']) qp.setPen(QPen(highlight_color, 2)) qp.fillPath(path, bg_color) # add close button circle_size = rect.height() / 1.8 pen_size = 2 qp.setPen(QPen(text_color, pen_size, Qt.SolidLine)) rect = QRectF( rect.right() - circle_size - self.settings['padding-x'] / 2, rect.top() + (rect.height() - circle_size) / 2, circle_size, circle_size) path.addEllipse(rect) qp.drawPath(path) # draw cross inside_rect = QRectF(rect) inside_rect.adjust(pen_size, pen_size, -pen_size, -pen_size) qp.drawLine(inside_rect.topLeft(), inside_rect.bottomRight()) qp.drawLine(inside_rect.bottomLeft(), inside_rect.topRight()) self.close_rectangles[text] = rect
def drawNode(painter, isSelected, node): rect = NodePainter.getNodeDim(node) if isSelected: painter.setPen(QPen(Qt.cyan, 3.0)) else: painter.setPen(Qt.black) painter.setBrush(Qt.gray) painter.drawRect(rect) for index, port in enumerate(node.data.inputs): NodePainter.drawPort(painter, False, index, node.data.getInput(index).value) # for index, port in enumerate(node.node.value): # NodePainter.drawPort(painter, False, index, node.data.getOutput(index)) NodePainter.drawPort(painter, True, 0, node.data.getOutput(0)) metrics = QFontMetrics(painter.font()) textBounds = metrics.boundingRect(node.title) painter.setPen(QColor(0, 0, 0, 128)) painter.setBrush(QColor(0, 0, 0, 128)) textRect = QRectF((rect.width() - textBounds.width()) / 2.0, (rect.height() - textBounds.height()) / 2.0 - 3.5, textBounds.width(), textBounds.height()) painter.drawText(textRect.bottomLeft(), node.title)
class MapObjectOutline(QGraphicsItem): def __init__(self, object, parent=None): super().__init__(parent) self.mObject = object self.mBoundingRect = QRectF() self.setZValue(1) # makes sure outlines are above labels def syncWithMapObject(self, renderer): pixelPos = renderer.pixelToScreenCoords_(self.mObject.position()) bounds = objectBounds(self.mObject, renderer) bounds.translate(-pixelPos) self.setPos(pixelPos + self.mObject.objectGroup().offset()) self.setRotation(self.mObject.rotation()) if (self.mBoundingRect != bounds): self.prepareGeometryChange() self.mBoundingRect = bounds def boundingRect(self): return self.mBoundingRect def paint(self, painter, arg2, arg3): horizontal = [ QLineF(self.mBoundingRect.topRight(), self.mBoundingRect.topLeft()), QLineF(self.mBoundingRect.bottomRight(), self.mBoundingRect.bottomLeft()) ] vertical = [ QLineF(self.mBoundingRect.bottomLeft(), self.mBoundingRect.topLeft()), QLineF(self.mBoundingRect.bottomRight(), self.mBoundingRect.topRight()) ] dashPen = QPen(Qt.DashLine) dashPen.setCosmetic(True) dashPen.setDashOffset(max(0.0, self.x())) painter.setPen(dashPen) painter.drawLines(horizontal) dashPen.setDashOffset(max(0.0, self.y())) painter.setPen(dashPen) painter.drawLines(vertical)
def draw(self, p: QtGui.QPainter, rect: QtCore.QRectF) -> None: p.setPen(self._pen) if self._orient_v == 'bottom': lp, rp = rect.topLeft(), rect.topRight() # p.drawLine(rect.topLeft(), rect.topRight()) elif self._orient_v == 'top': lp, rp = rect.bottomLeft(), rect.bottomRight() p.drawLine(lp.x(), lp.y(), rp.x(), rp.y())
def qgraphicsview_map_rect_from_scene(view: QGraphicsView, rect: QRectF) -> QPolygonF: """Like QGraphicsView.mapFromScene(QRectF) but returning a QPolygonF (without rounding). """ tr = view.viewportTransform() p1 = tr.map(rect.topLeft()) p2 = tr.map(rect.topRight()) p3 = tr.map(rect.bottomRight()) p4 = tr.map(rect.bottomLeft()) return QPolygonF([p1, p2, p3, p4])
def get_sides_of(rect: QRectF): """ This method returns the sides of a rect as a dictionary. Parameters ---------- rect : QRectF The rect to inspect. Returns ---------- dict A container of the sides represented as QLineF objects and their position as a key. """ return { "top": QLineF(rect.topLeft(), rect.topRight()), "right": QLineF(rect.topRight(), rect.bottomRight()), "bottom": QLineF(rect.bottomLeft(), rect.bottomRight()), "left": QLineF(rect.bottomLeft(), rect.topLeft()) }
class MapObjectOutline(QGraphicsItem): def __init__(self, object, parent = None): super().__init__(parent) self.mObject = object self.mBoundingRect = QRectF() self.setZValue(1) # makes sure outlines are above labels def syncWithMapObject(self, renderer): pixelPos = renderer.pixelToScreenCoords_(self.mObject.position()) bounds = objectBounds(self.mObject, renderer) bounds.translate(-pixelPos) self.setPos(pixelPos + self.mObject.objectGroup().offset()) self.setRotation(self.mObject.rotation()) if (self.mBoundingRect != bounds): self.prepareGeometryChange() self.mBoundingRect = bounds def boundingRect(self): return self.mBoundingRect def paint(self, painter, arg2, arg3): horizontal = [ QLineF(self.mBoundingRect.topRight(), self.mBoundingRect.topLeft()), QLineF(self.mBoundingRect.bottomRight(), self.mBoundingRect.bottomLeft())] vertical = [ QLineF(self.mBoundingRect.bottomLeft(), self.mBoundingRect.topLeft()), QLineF(self.mBoundingRect.bottomRight(), self.mBoundingRect.topRight())] dashPen = QPen(Qt.DashLine) dashPen.setCosmetic(True) dashPen.setDashOffset(max(0.0, self.x())) painter.setPen(dashPen) painter.drawLines(horizontal) dashPen.setDashOffset(max(0.0, self.y())) painter.setPen(dashPen) painter.drawLines(vertical)
def _paint_stimulus_points_cell(self, painter, option, model_index): points = model_index.data( role=ProtocolSequence.Roles.STIMULUS_POINTS.value) if points: font_metrics = QFontMetrics(option.font) font_height = font_metrics.height() pixmap_plus1 = QPixmap() pixmap_plus1.load(Resources.get('one.png')) pixmap_random = QPixmap() pixmap_random.load(Resources.get('dice.png')) padding = 1 max_img_height = max(pixmap_plus1.height(), pixmap_random.height()) max_img_width = max(pixmap_plus1.width(), pixmap_random.width()) cell_centre = (option.rect.x() + 0.5 * option.rect.width(), option.rect.y() + 0.5 * option.rect.height()) container_height = font_height + max_img_height + 2 * padding container_width = max_img_width + 2 * padding start_x = cell_centre[0] - container_width * (len(points) / 2) container = QRectF(start_x, cell_centre[1] - 0.5 * container_height, container_width, container_height) self.container_height = max(self.container_height, container.height()) for point in points: pixmap = point.pattern.pixmap(max_img_height) painter.drawPixmap(container.topLeft().x() + padding, container.topLeft().y() + padding, pixmap) text = str(point.index()) text_bounding_rect = font_metrics.boundingRect(text) painter.drawText( container.center().x() - 0.5 * text_bounding_rect.width(), container.bottomLeft().y() - padding, text) container.translate(container_width, 0) self.sizeHintChanged.emit(model_index)
def _debug_draw(self, painter, option, model_index): num_points = 9 font_metrics = QFontMetrics(option.font) pixmap = QPixmap() pixmap.load(Resources.get('dice.png')) padding = 1 max_img_height = pixmap.height() cell_centre = (option.rect.x() + 0.5 * option.rect.width(), option.rect.y() + 0.5 * option.rect.height()) font_height = font_metrics.height() container_height = font_height + max_img_height + 2 * padding container_width = pixmap.width() + 2 * padding start_x = cell_centre[0] - container_width * (num_points / 2) painter.drawLine(QPointF(cell_centre[0], 0), QPointF(cell_centre[0], option.rect.height())) painter.drawLine(QPointF(0, cell_centre[1]), QPointF(option.rect.width(), cell_centre[1])) container = QRectF(start_x, cell_centre[1] - 0.5 * container_height, container_width, container_height) for i in range(0, num_points): painter.drawRect(container) painter.drawPixmap(container.topLeft().x() + padding, container.topLeft().y() + padding, pixmap) text = str(i) text_bounding_rect = font_metrics.boundingRect(text) painter.drawText( container.center().x() - 0.5 * text_bounding_rect.width(), container.bottomLeft().y() - padding, text) container.translate(container_width, 0)
def redraw(self): self.graphicsScene.clear() # draw screenshot self.graphicsScene.addPixmap(self.screenPixel) # prepare for drawing selected area rect = QRectF(self.selectedArea) rect = rect.normalized() topLeftPoint = rect.topLeft() topRightPoint = rect.topRight() bottomLeftPoint = rect.bottomLeft() bottomRightPoint = rect.bottomRight() topMiddlePoint = (topLeftPoint + topRightPoint) / 2 leftMiddlePoint = (topLeftPoint + bottomLeftPoint) / 2 bottomMiddlePoint = (bottomLeftPoint + bottomRightPoint) / 2 rightMiddlePoint = (topRightPoint + bottomRightPoint) / 2 # draw the picture mask mask = QColor(0, 0, 0, 155) if self.selectedArea == QRect(): self.graphicsScene.addRect(0, 0, self.screenPixel.width(), self.screenPixel.height(), QPen(Qt.NoPen), mask) else: self.graphicsScene.addRect(0, 0, self.screenPixel.width(), topRightPoint.y(), QPen(Qt.NoPen), mask) self.graphicsScene.addRect(0, topLeftPoint.y(), topLeftPoint.x(), rect.height(), QPen(Qt.NoPen), mask) self.graphicsScene.addRect( topRightPoint.x(), topRightPoint.y(), self.screenPixel.width() - topRightPoint.x(), rect.height(), QPen(Qt.NoPen), mask) self.graphicsScene.addRect( 0, bottomLeftPoint.y(), self.screenPixel.width(), self.screenPixel.height() - bottomLeftPoint.y(), QPen(Qt.NoPen), mask) # draw the toolBar if self.action != ACTION_SELECT: spacing = 5 # show the toolbar first, then move it to the correct position # because the width of it may be wrong if this is the first time it shows self.tooBar.show() dest = QPointF(rect.bottomRight() - QPointF(self.tooBar.width(), 0) - QPointF(spacing, -spacing)) if dest.x() < spacing: dest.setX(spacing) pen_set_bar_height = self.penSetBar.height( ) if self.penSetBar is not None else 0 if dest.y() + self.tooBar.height( ) + pen_set_bar_height >= self.height(): if rect.top() - self.tooBar.height( ) - pen_set_bar_height < spacing: dest.setY(rect.top() + spacing) else: dest.setY(rect.top() - self.tooBar.height() - pen_set_bar_height - spacing) self.tooBar.move(dest.toPoint()) if self.penSetBar is not None: self.penSetBar.show() self.penSetBar.move(dest.toPoint() + QPoint(0, self.tooBar.height() + spacing)) if self.action == ACTION_TEXT: self.penSetBar.showFontWidget() else: self.penSetBar.showPenWidget() else: self.tooBar.hide() if self.penSetBar is not None: self.penSetBar.hide() # draw the list for step in self.drawListResult: self.drawOneStep(step) if self.drawListProcess is not None: self.drawOneStep(self.drawListProcess) if self.action != ACTION_TEXT: self.drawListProcess = None if self.selectedArea != QRect(): self.itemsToRemove = [] # draw the selected rectangle pen = QPen(QColor(0, 255, 255), 2) self.itemsToRemove.append(self.graphicsScene.addRect(rect, pen)) # draw the drag point radius = QPoint(3, 3) brush = QBrush(QColor(0, 255, 255)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(topLeftPoint - radius, topLeftPoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(topMiddlePoint - radius, topMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(topRightPoint - radius, topRightPoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(leftMiddlePoint - radius, leftMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(rightMiddlePoint - radius, rightMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(bottomLeftPoint - radius, bottomLeftPoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(bottomMiddlePoint - radius, bottomMiddlePoint + radius), pen, brush)) self.itemsToRemove.append( self.graphicsScene.addEllipse( QRectF(bottomRightPoint - radius, bottomRightPoint + radius), pen, brush)) # draw the textedit if self.textPosition is not None: textSpacing = 50 position = QPoint() if self.textPosition.x() + self.textInput.width( ) >= self.screenPixel.width(): position.setX(self.textPosition.x() - self.textInput.width()) else: position.setX(self.textPosition.x()) if self.textRect is not None: if self.textPosition.y() + self.textInput.height( ) + self.textRect.height() >= self.screenPixel.height(): position.setY(self.textPosition.y() - self.textInput.height() - self.textRect.height()) else: position.setY(self.textPosition.y() + self.textRect.height()) else: if self.textPosition.y() + self.textInput.height( ) >= self.screenPixel.height(): position.setY(self.textPosition.y() - self.textInput.height()) else: position.setY(self.textPosition.y()) self.textInput.move(position) self.textInput.show() # self.textInput.getFocus() # draw the magnifier if self.action == ACTION_SELECT: self.drawMagnifier() if self.mousePressed: self.drawSizeInfo() if self.action == ACTION_MOVE_SELECTED: self.drawSizeInfo()
def drawMagnifier(self): # First, calculate the magnifier position due to the mouse position watchAreaWidth = 16 watchAreaHeight = 16 watchAreaPixmap = QPixmap() cursor_pos = self.mousePoint watchArea = QRect( QPoint(cursor_pos.x() - watchAreaWidth / 2, cursor_pos.y() - watchAreaHeight / 2), QPoint(cursor_pos.x() + watchAreaWidth / 2, cursor_pos.y() + watchAreaHeight / 2)) if watchArea.left() < 0: watchArea.moveLeft(0) watchArea.moveRight(watchAreaWidth) if self.mousePoint.x() + watchAreaWidth / 2 >= self.screenPixel.width( ): watchArea.moveRight(self.screenPixel.width() - 1) watchArea.moveLeft(watchArea.right() - watchAreaWidth) if self.mousePoint.y() - watchAreaHeight / 2 < 0: watchArea.moveTop(0) watchArea.moveBottom(watchAreaHeight) if self.mousePoint.y( ) + watchAreaHeight / 2 >= self.screenPixel.height(): watchArea.moveBottom(self.screenPixel.height() - 1) watchArea.moveTop(watchArea.bottom() - watchAreaHeight) # tricks to solve the hidpi impact on QCursor.pos() watchArea.setTopLeft( QPoint(watchArea.topLeft().x() * self.scale, watchArea.topLeft().y() * self.scale)) watchArea.setBottomRight( QPoint(watchArea.bottomRight().x() * self.scale, watchArea.bottomRight().y() * self.scale)) watchAreaPixmap = self.screenPixel.copy(watchArea) # second, calculate the magnifier area magnifierAreaWidth = watchAreaWidth * 10 magnifierAreaHeight = watchAreaHeight * 10 fontAreaHeight = 40 cursorSize = 24 magnifierArea = QRectF( QPoint(QCursor.pos().x() + cursorSize, QCursor.pos().y() + cursorSize), QPoint(QCursor.pos().x() + cursorSize + magnifierAreaWidth, QCursor.pos().y() + cursorSize + magnifierAreaHeight)) if magnifierArea.right() >= self.screenPixel.width(): magnifierArea.moveLeft(QCursor.pos().x() - magnifierAreaWidth - cursorSize / 2) if magnifierArea.bottom() + fontAreaHeight >= self.screenPixel.height( ): magnifierArea.moveTop(QCursor.pos().y() - magnifierAreaHeight - cursorSize / 2 - fontAreaHeight) # third, draw the watch area to magnifier area watchAreaScaled = watchAreaPixmap.scaled( QSize(magnifierAreaWidth * self.scale, magnifierAreaHeight * self.scale)) magnifierPixmap = self.graphicsScene.addPixmap(watchAreaScaled) magnifierPixmap.setOffset(magnifierArea.topLeft()) # then draw lines and text self.graphicsScene.addRect(QRectF(magnifierArea), QPen(QColor(255, 255, 255), 2)) self.graphicsScene.addLine( QLineF(QPointF(magnifierArea.center().x(), magnifierArea.top()), QPointF(magnifierArea.center().x(), magnifierArea.bottom())), QPen(QColor(0, 255, 255), 2)) self.graphicsScene.addLine( QLineF(QPointF(magnifierArea.left(), magnifierArea.center().y()), QPointF(magnifierArea.right(), magnifierArea.center().y())), QPen(QColor(0, 255, 255), 2)) # get the rgb of mouse point pointRgb = QColor(self.screenPixel.toImage().pixel(self.mousePoint)) # draw information self.graphicsScene.addRect( QRectF( magnifierArea.bottomLeft(), magnifierArea.bottomRight() + QPoint(0, fontAreaHeight + 30)), Qt.black, QBrush(Qt.black)) rgbInfo = self.graphicsScene.addSimpleText( ' Rgb: ({0}, {1}, {2})'.format(pointRgb.red(), pointRgb.green(), pointRgb.blue())) rgbInfo.setPos(magnifierArea.bottomLeft() + QPoint(0, 5)) rgbInfo.setPen(QPen(QColor(255, 255, 255), 2)) rect = self.selectedArea.normalized() sizeInfo = self.graphicsScene.addSimpleText(' Size: {0} x {1}'.format( rect.width() * self.scale, rect.height() * self.scale)) sizeInfo.setPos(magnifierArea.bottomLeft() + QPoint(0, 15) + QPoint(0, fontAreaHeight / 2)) sizeInfo.setPen(QPen(QColor(255, 255, 255), 2))
# construct paths for breakpoint handles def _hashMarkGen(path, p1, p2, p3): path.moveTo(p1) path.lineTo(p2) path.lineTo(p3) # end # create hash marks QPainterPaths only once _PP_RECT = QRectF(0, 0, styles.PATH_BASE_WIDTH, styles.PATH_BASE_WIDTH) _PATH_CENTER = QPointF(styles.PATH_BASE_WIDTH / 2,\ styles.PATH_BASE_WIDTH / 2) _PATH_U_CENTER = QPointF(styles.PATH_BASE_WIDTH / 2, 0) _PATH_D_CENTER = QPointF(styles.PATH_BASE_WIDTH / 2, styles.PATH_BASE_WIDTH) _PPATH_LU = QPainterPath() _hashMarkGen(_PPATH_LU, _PP_RECT.bottomLeft(), _PATH_D_CENTER, _PATH_CENTER) _PPATH_RU = QPainterPath() _hashMarkGen(_PPATH_RU, _PP_RECT.bottomRight(), _PATH_D_CENTER, _PATH_CENTER) _PPATH_RD = QPainterPath() _hashMarkGen(_PPATH_RD, _PP_RECT.topRight(), _PATH_U_CENTER, _PATH_CENTER) _PPATH_LD = QPainterPath() _hashMarkGen(_PPATH_LD, _PP_RECT.topLeft(), _PATH_U_CENTER, _PATH_CENTER) _SCAF_PEN = QPen(styles.PXI_SCAF_STROKE, styles.PATH_STRAND_STROKE_WIDTH) _SCAF_PEN.setCapStyle(Qt.FlatCap) # or Qt.RoundCap _SCAF_PEN.setJoinStyle(Qt.RoundJoin) _STAP_PEN = QPen(styles.PXI_STAP_STROKE, styles.PATH_STRAND_STROKE_WIDTH) _STAP_PEN.setCapStyle(Qt.FlatCap) # or Qt.RoundCap _STAP_PEN.setJoinStyle(Qt.RoundJoin) _DISAB_PEN = QPen(styles.PXI_DISAB_STROKE, styles.PATH_STRAND_STROKE_WIDTH) _DISAB_PEN.setCapStyle(Qt.FlatCap)
class PangoBboxGraphic(PangoGraphic): def __init__(self, parent=None): super().__init__(parent) self.setFlag(QGraphicsItem.ItemIsSelectable) self.fpath = None self.rect = QRectF() def paint(self, painter, option, widget): super().paint(painter, option, widget) w = self.dw() pen = self.pen() pen.setWidth(int(w)) painter.setPen(pen) if not self.force_opaque: painter.setOpacity(0.8) painter.drawRect(self.rect) if self.parentItem() is not None: self.paint_text_rect(painter) painter.setOpacity(1) if option.state & QStyle.State_MouseOver or self.isSelected(): painter.drawEllipse(self.rect.topLeft(), w, w) painter.drawEllipse(self.rect.topRight(), w, w) painter.drawEllipse(self.rect.bottomLeft(), w, w) painter.drawEllipse(self.rect.bottomRight(), w, w) def paint_text_rect(self, painter): p = painter.pen() font = QFont() font.setPointSizeF(self.dw()*3) painter.setFont(font) painter.setBrush(self.brush()) fm = QFontMetrics(font) w = fm.width(self.parentItem().name) h = fm.height() br = self.rect.bottomRight() text_rect = QRectF(QPointF(br.x()-w, br.y()-h), br) if text_rect.width() < self.boundingRect().width()/2 and\ text_rect.height() < self.boundingRect().height()/2: painter.drawRect(text_rect) pen = self.pen() pen.setColor(QColor("black")) painter.setPen(pen) painter.drawText(text_rect, Qt.AlignCenter, self.parentItem().name) painter.setPen(p) def boundingRect(self): w = self.dw() return self.shape().controlPointRect().adjusted(-w*2, -w*2, w*2, w*2) def shape(self): path = QPainterPath() path.addRect(self.rect) return self.shape_from_path(path, self.pen())
class TextBoxItem(QGraphicsItem): max_rect = QRect(-56, -20, 112, 40) init_offset = QPointF(66, -34) dummy_contents = 'XX-ABCDE ####\n##### xxxxx\n10000 = 10000 X####' dummy_contents_compact = 'XX-ABCDE\n10000 #### ' txt_rect_2lines = QRectF() # STATIC txt_rect_3lines = QRectF() # STATIC def setRectanglesFromFont(font): TextBoxItem.txt_rect_2lines = QRectF( QFontMetrics(font).boundingRect( TextBoxItem.max_rect, Qt.AlignLeft, TextBoxItem.dummy_contents_compact)) TextBoxItem.txt_rect_3lines = QRectF( QFontMetrics(font).boundingRect(TextBoxItem.max_rect, Qt.AlignLeft, TextBoxItem.dummy_contents)) def __init__(self, parent_item): QGraphicsItem.__init__(self, parent_item) self.radar_contact = parent_item.radar_contact self.info_text = '' self.rectangle = QRectF() self.setCursor(Qt.PointingHandCursor) self.setPos(TextBoxItem.init_offset) self.mouse_hovering = False self.paint_border = True self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setAcceptHoverEvents(True) self.updateContents() def updateContents(self): self.prepareGeometryChange() expand = self.mouse_hovering or self.radar_contact is selection.acft or env.linkedStrip( self.radar_contact) != None self.paint_border = expand self.info_text = infoTextLines(self.radar_contact, not expand) self.rectangle = TextBoxItem.txt_rect_3lines if expand else TextBoxItem.txt_rect_2lines def positionQuadrant(self): return (1 if self.pos().x() > 0 else -1), (1 if self.pos().y() > 0 else -1) def calloutConnectingPoint(self): q = self.positionQuadrant() if q == (-1, -1): return self.rectangle.bottomRight() elif q == (-1, 1): return self.rectangle.topRight() elif q == (1, -1): return self.rectangle.bottomLeft() elif q == (1, 1): return self.rectangle.topLeft() def paint(self, painter, option, widget): coloured_pen = new_pen(ACFT_pen_colour(self.radar_contact)) # 1. Write info text painter.setPen(coloured_pen) painter.drawText(self.rectangle, Qt.AlignLeft | Qt.AlignVCenter, self.info_text) # 2. Draw container box? if self.paint_border: pen = coloured_pen if self.radar_contact is selection.acft else new_pen( settings.colour('radar_tag_line')) if self.radar_contact.individual_cheat: pen.setStyle(Qt.DashLine) painter.setPen(pen) painter.drawRect(self.rectangle) def boundingRect(self): return self.rectangle # EVENTS def itemChange(self, change, value): if change == QGraphicsItem.ItemPositionChange: self.parentItem().textBoxChanged() return QGraphicsItem.itemChange(self, change, value) def hoverEnterEvent(self, event): self.mouse_hovering = True self.updateContents() self.parentItem().textBoxChanged() QGraphicsItem.hoverEnterEvent(self, event) def hoverLeaveEvent(self, event): self.mouse_hovering = False self.updateContents() self.parentItem().textBoxChanged() QGraphicsItem.hoverLeaveEvent(self, event) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: selection.selectAircraft(self.radar_contact) elif event.button() == Qt.MiddleButton: if event.modifiers() & Qt.ShiftModifier: selection.unlinkAircraft(self.radar_contact) else: selection.linkAircraft(self.radar_contact) event.accept() QGraphicsItem.mousePressEvent(self, event) def mouseDoubleClickEvent(self, event): if event.button() == Qt.LeftButton: if event.modifiers() & Qt.ShiftModifier: # reset box position self.setPos(TextBoxItem.init_offset) self.parentItem().textBoxChanged() else: strip = selection.strip if strip != None: signals.stripEditRequest.emit(strip) event.accept() else: QGraphicsItem.mouseDoubleClickEvent(self, event)
class SelectionTool(BaseTool): name = "Selection" iconPath = ":/resources/cursor.svg" def __init__(self, parent=None): super().__init__(parent) self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self._shouldPrepareUndo = False # helpers def _createAnchor(self, *args): widget = self.parent() pos = widget.mapToCanvas(widget.mapFromGlobal(self._cachedPos)) newAnchorName, ok = AddAnchorDialog.getNewAnchorName(widget, pos) if ok: anchor = TAnchor() anchor.x = pos.x() anchor.y = pos.y() anchor.name = newAnchorName self._glyph.appendAnchor(anchor) def _createComponent(self, *args): widget = self.parent() newGlyph, ok = AddComponentDialog.getNewGlyph(widget, self._glyph) if ok and newGlyph is not None: component = TComponent() component.baseGlyph = newGlyph.name self._glyph.appendComponent(component) def _getSelectedCandidatePoint(self): """ If there is exactly one point selected in the glyph, return it. Else return None. """ candidates = set() for contour in self._glyph: sel = contour.selection if len(sel) > 1: return None elif not len(sel): continue pt = next(iter(sel)) candidates.add((pt, contour)) if len(candidates) == 1: return next(iter(candidates)) return None def _moveForEvent(self, event): key = event.key() modifiers = event.modifiers() dx, dy = 0, 0 if key == Qt.Key_Left: dx = -1 elif key == Qt.Key_Up: dy = 1 elif key == Qt.Key_Right: dx = 1 elif key == Qt.Key_Down: dy = -1 if modifiers & Qt.ShiftModifier: dx *= 10 dy *= 10 if modifiers & Qt.ControlModifier: dx *= 10 dy *= 10 return (dx, dy) # actions def showContextMenu(self, pos): self._cachedPos = pos menu = QMenu(self.parent()) menu.addAction("Add Anchor…", self._createAnchor) menu.addAction("Add Component…", self._createComponent) menu.exec_(self._cachedPos) self._cachedPos = None # events def keyPressEvent(self, event): key = event.key() if key == platformSpecific.deleteKey: glyph = self._glyph # TODO: prune glyph.prepareUndo() preserveShape = not event.modifiers() & Qt.ShiftModifier for anchor in glyph.anchors: if anchor.selected: glyph.removeAnchor(anchor) for contour in reversed(glyph): removeUISelection(contour, preserveShape) for component in glyph.components: if component.selected: glyph.removeComponent(component) elif key in arrowKeys: # TODO: prune self._glyph.prepareUndo() delta = self._moveForEvent(event) # TODO: seems weird that glyph.selection and selected don't incl. # anchors and components while glyph.move does... see what glyphs # does hadSelection = False for anchor in self._glyph.anchors: if anchor.selected: anchor.move(delta) hadSelection = True for contour in self._glyph: moveUISelection(contour, delta) # XXX: shouldn't have to recalc this if contour.selection: hadSelection = True for component in self._glyph.components: if component.selected: component.move(delta) hadSelection = True if not hadSelection: event.ignore() elif key in navKeys: pack = self._getSelectedCandidatePoint() if pack is not None: point, contour = pack point.selected = False index = contour.index(point) offset = int(key == Qt.Key_Greater) or -1 newPoint = contour.getPoint(index + offset) newPoint.selected = True contour.postNotification( notification="Contour.SelectionChanged") def mousePressEvent(self, event): if event.button() & Qt.RightButton: self.showContextMenu(event.globalPos()) return widget = self.parent() addToSelection = event.modifiers() & Qt.ShiftModifier self._origin = event.localPos() self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: itemUnderMouse, parentContour = self._itemTuple if not (itemUnderMouse.selected or addToSelection): for anchor in self._glyph.anchors: anchor.selected = False for component in self._glyph.components: component.selected = False self._glyph.selected = False itemUnderMouse.selected = True if parentContour is not None: parentContour.postNotification( notification="Contour.SelectionChanged") self._shouldPrepareUndo = True else: if addToSelection: self._oldSelection = self._glyph.selection else: for anchor in self._glyph.anchors: anchor.selected = False for component in self._glyph.components: component.selected = False self._glyph.selected = False widget.update() def mouseMoveEvent(self, event): canvasPos = event.localPos() widget = self.parent() if self._itemTuple is not None: if self._shouldPrepareUndo: self._glyph.prepareUndo() self._shouldPrepareUndo = False dx = canvasPos.x() - self._origin.x() dy = canvasPos.y() - self._origin.y() for anchor in self._glyph.anchors: if anchor.selected: anchor.move((dx, dy)) for contour in self._glyph: moveUISelection(contour, (dx, dy)) for component in self._glyph.components: if component.selected: component.move((dx, dy)) self._origin = canvasPos else: self._rubberBandRect = QRectF(self._origin, canvasPos).normalized() items = widget.items(self._rubberBandRect) points = set(items["points"]) if event.modifiers() & Qt.ShiftModifier: points ^= self._oldSelection # TODO: fine-tune this more, maybe add optional args to items... if event.modifiers() & Qt.AltModifier: points = set(pt for pt in points if pt.segmentType) if points != self._glyph.selection: # TODO: doing this takes more time than by-contour # discrimination for large point count self._glyph.selection = points widget.update() def mouseReleaseEvent(self, event): self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self.parent().update() def mouseDoubleClickEvent(self, event): widget = self.parent() self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: item, parent = self._itemTuple if parent is None: return point, contour = item, parent if point.segmentType is not None: self._glyph.prepareUndo() point.smooth = not point.smooth contour.dirty = True # custom painting def paint(self, painter): if self._rubberBandRect is None: return widget = self.parent() # okay, OS-native rubber band does not support painting with # floating-point coordinates # paint directly on the widget with unscaled context widgetOrigin = widget.mapToWidget(self._rubberBandRect.bottomLeft()) widgetMove = widget.mapToWidget(self._rubberBandRect.topRight()) option = QStyleOptionRubberBand() option.initFrom(widget) option.opaque = False option.rect = QRectF(widgetOrigin, widgetMove).toRect() option.shape = QRubberBand.Rectangle painter.save() painter.setRenderHint(QPainter.Antialiasing, False) painter.setTransform(QTransform()) widget.style().drawControl( QStyle.CE_RubberBand, option, painter, widget) painter.restore()
class JoyPad(QtWidgets.QWidget, IndicatorPosition): IndicatorPosition = IndicatorPosition Q_ENUM(IndicatorPosition) joy_btn_pressed = QtCore.pyqtSignal(str) joy_btn_released = QtCore.pyqtSignal(str) joy_l_pressed = QtCore.pyqtSignal(bool) joy_l_released = QtCore.pyqtSignal(bool) joy_r_pressed = QtCore.pyqtSignal(bool) joy_r_released = QtCore.pyqtSignal(bool) joy_c_pressed = QtCore.pyqtSignal(bool) joy_c_released = QtCore.pyqtSignal(bool) joy_t_pressed = QtCore.pyqtSignal(bool) joy_t_released = QtCore.pyqtSignal(bool) joy_b_pressed = QtCore.pyqtSignal(bool) joy_b_released = QtCore.pyqtSignal(bool) def __init__(self, parent=None): super(JoyPad, self).__init__(parent) self.rect1 = QRectF() self.rect2 = QRectF() self.left_image = None self.right_image = None self.top_image = None self.bottom_image = None self.center_image = None self._dummyPixmap = QtGui.QPixmap() self._font = QFont('Lato Heavy', 20) self._text_color = QColor('white') self._textL = '' self._textR = '' self._textC = '' self._textT = '' self._textB = '' self.colorState = False self._true_color = QColor('lawngreen') self._false_color = QColor('gray') self.highlight_color = self._false_color self._highlightPosition = IndicatorPosition.NONE self.highlight_left = False self.highlight_right = False self.highlight_top = False self.highlight_bottom = False self.highlight_center = False self.last_active_btn = None self.setMouseTracking(True) self.setToolTipDuration(2000) self.installEventFilter(self) self.btn_names = { 'L': 'left', 'R': 'right', 'T': 'top', 'B': 'bottom', 'C': 'center' } self.tooltips = {'L': '', 'R': '', 'T': '', 'B': '', 'C': ''} self.axis_list = ('X', 'Y', 'Z', 'A') def eventFilter(self, obj, event): if obj is self and self.isEnabled(): if event.type() == QEvent.MouseButtonPress: if event.button() == Qt.RightButton: event.ignore() else: pos = event.localPos() active_btn = self.get_active_btn(pos) self.last_active_btn = active_btn if active_btn is not None: self._pressedOutput(active_btn) elif event.type() == QEvent.MouseButtonRelease: if event.button() == Qt.RightButton: event.ignore() elif self.last_active_btn is not None: self._releasedOutput(self.last_active_btn) elif event.type() == QEvent.MouseMove: pos = event.pos() active_btn = self.get_active_btn(pos) if active_btn is not None: self.setToolTip(self.tooltips[active_btn]) return super(JoyPad, self).eventFilter(obj, event) def _pressedOutput(self, btncode): self.joy_btn_pressed.emit(btncode) self['joy_{}_pressed'.format(btncode.lower())].emit(True) def _releasedOutput(self, btncode): self.joy_btn_released.emit(btncode) self['joy_{}_released'.format(btncode.lower())].emit(False) def get_active_btn(self, pos): if self.center_path.contains(pos): return 'C' elif self.left_path.contains(pos): return 'L' elif self.right_path.contains(pos): return 'R' elif self.bottom_path.contains(pos): return 'B' elif self.top_path.contains(pos): return 'T' return None def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(painter.Antialiasing) w = min(event.rect().width(), event.rect().height()) self.rect1.setSize(QSizeF(w * 0.4, w * 0.4)) self.rect2.setSize(QSizeF(w * 0.9, w * 0.9)) self.create_paths(painter, event) self.draw_painter_paths(painter, event) self.draw_icons(painter, event) self.draw_highlight(painter, event) painter.end() def create_paths(self, qp, event): self.left_path = QPainterPath() self.right_path = QPainterPath() self.bottom_path = QPainterPath() self.top_path = QPainterPath() self.center_path = QPainterPath() center = event.rect().center() self.rect1.moveCenter(center) self.rect2.moveCenter(center) left_start = QPointF(self.rect1.topLeft()) right_start = QPointF(self.rect1.bottomRight()) bottom_start = QPointF(self.rect1.bottomLeft()) top_start = QPointF(self.rect1.topRight()) path = (self.right_path, self.top_path, self.left_path, self.bottom_path) start = (right_start, top_start, left_start, bottom_start) angle = -45 for i in range(4): path[i].moveTo(start[i]) path[i].arcTo(self.rect1, angle, 90) path[i].arcTo(self.rect2, angle + 90, -90) path[i].closeSubpath() angle += 90 cap = QRectF() cap.setSize(QSizeF(self.rect1.width() * 0.8, self.rect1.height() * 0.8)) cap.moveCenter(center) self.center_path.addEllipse(cap) def draw_painter_paths(self, qp, event): w = min(event.rect().width(), event.rect().height()) center = event.rect().center() fp = QPoint(int(center.x() - w / 4), int(center.y() - w / 4)) bg = QRadialGradient(center, w / 2, fp) bg.setColorAt(0, QColor(180, 180, 180)) bg.setColorAt(1, QColor(40, 40, 40)) qp.setBrush(QBrush(bg)) qp.setPen(QPen(QColor(Qt.black), 4)) qp.drawPath(self.left_path) qp.drawPath(self.right_path) qp.drawPath(self.top_path) qp.drawPath(self.bottom_path) qp.drawPath(self.center_path) def draw_icons(self, qp, event): rect = QRect() rect.setSize( QSize(int(self.rect1.width() * 0.4), int(self.rect1.height() * 0.4))) center = event.rect().center() qp.setPen(QPen(self._text_color, 2)) qp.setFont(self._font) # left button rect.moveCenter( QPoint(int(center.x() - self.rect2.width() / 3), center.y())) if isinstance(self.left_image, QPixmap): pix = self.left_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.left_image, str): qp.drawText(rect, Qt.AlignCenter, self.left_image) # right button rect.moveCenter( QPoint(int(center.x() + self.rect2.width() / 3), center.y())) if isinstance(self.right_image, QPixmap): pix = self.right_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.right_image, str): qp.drawText(rect, Qt.AlignCenter, self.right_image) # bottom button rect.moveCenter( QPoint(center.x(), int(center.y() + self.rect2.width() / 3))) if isinstance(self.bottom_image, QPixmap): pix = self.bottom_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.bottom_image, str): qp.drawText(rect, Qt.AlignCenter, self.bottom_image) # top button rect.moveCenter( QPoint(center.x(), int(center.y() - self.rect2.width() / 3))) if isinstance(self.top_image, QPixmap): pix = self.top_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.top_image, str): qp.drawText(rect, Qt.AlignCenter, self.top_image) # center button rect.moveCenter(QPoint(center.x(), center.y())) if isinstance(self.center_image, QPixmap): pix = self.center_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.center_image, str): qp.drawText(rect, Qt.AlignCenter, self.center_image) def draw_highlight(self, qp, event): rect = QRectF() rect.setSize(self.rect1.size() * 0.9) center = event.rect().center() rect.moveCenter(center) pen_width = self.rect1.width() * 0.08 qp.setPen(QPen(self.highlight_color, pen_width, cap=Qt.FlatCap)) if self.highlight_center is True: qp.drawArc(rect, 0, 5760) else: if self.highlight_right is True: qp.drawArc(rect, -45 * 16, 90 * 16) if self.highlight_left is True: qp.drawArc(rect, 135 * 16, 90 * 16) if self.highlight_top is True: qp.drawArc(rect, 45 * 16, 90 * 16) if self.highlight_bottom is True: qp.drawArc(rect, 225 * 16, 90 * 16) def reset_highlight(self): self.highlight_left = False self.highlight_right = False self.highlight_top = False self.highlight_bottom = False self.highlight_center = False def set_highlight(self, btn, state=True): if type(btn) == int: if btn == IndicatorPosition.LEFT: btn = 'L' elif btn == IndicatorPosition.RIGHT: btn = 'R' elif btn == IndicatorPosition.CENTER: btn = 'C' elif btn == IndicatorPosition.TOP: btn = 'T' elif btn == IndicatorPosition.BOTTOM: btn = 'B' elif btn == IndicatorPosition.LEFTRIGHT: btn = 'X' elif btn == IndicatorPosition.TOPBOTTOM: btn = 'Z' elif btn == IndicatorPosition.NONE: self.reset_highlight() return else: print('undefined position:{}'.format(btn)) return if btn not in self.axis_list and btn not in self.btn_names.keys(): return if btn == 'X' or btn == 'A': self.highlight_left = state self.highlight_right = state elif btn == 'Y' or btn == 'Z': self.highlight_top = state self.highlight_bottom = state else: name = self.btn_names[btn] self['highlight_' + name] = state self.update() def set_button_icon(self, btn, path): self.set_icon(btn, 'image', path) def set_button_text(self, btn, text): self.set_icon(btn, 'text', text) def set_icon(self, btn, kind, data): if btn not in self.btn_names.keys(): return name = self.btn_names[btn] if kind == 'image': if data is None: self[name + "_image"] = None else: self[name + "_image"] = QPixmap(data) elif kind == 'text': self[name + "_image"] = data else: return self.update() def set_tooltip(self, btn, tip): if btn in self.btn_names.keys(): self.tooltips[btn] = tip def setLight(self, data): if data: self.highlight_color = self._true_color else: self.highlight_color = self._false_color self.update() def set_HighlightPosition(self, position): self._highlightPosition = position self.reset_highlight() self.set_highlight(position, True) self.update() def get_HighlightPosition(self): return self._highlightPosition def reset_HighlightPosition(self): self._highlightPosition = IndicatorPosition.NONE self.reset_highlight() self.update() highlightPosition = QtCore.pyqtProperty(IndicatorPosition, get_HighlightPosition, set_HighlightPosition, reset_HighlightPosition) @QtCore.pyqtSlot() def set_colorStateTrue(self): self.setLight(True) @QtCore.pyqtSlot() def set_colorStateFalse(self): self.setLight(False) @QtCore.pyqtSlot(bool) def set_colorState(self, state): self.colorState = bool(state) self.setLight(state) def get_colorState(self): return self.colorState def reset_colorState(self): self.colorState = False self.setLight(False) setColorState = QtCore.pyqtProperty(bool, get_colorState, set_colorState, reset_colorState) def setLeftImagePath(self, data): if data.isNull(): data = None self.set_icon('L', 'image', data) def getLeftImagePath(self): if isinstance(self.left_image, QPixmap): self.left_image else: return self._dummyPixmap def resetLeftImagePath(self): pass def setRightImagePath(self, data): if data.isNull(): data = None self.set_icon('R', 'image', data) def getRightImagePath(self): return self._dummyPixmap def resetRightImagePath(self): pass def setCenterImagePath(self, data): if data.isNull(): data = None self.set_icon('C', 'image', data) def getCenterImagePath(self): return self._dummyPixmap def resetCenterImagePath(self): pass def setTopImagePath(self, data): if data.isNull(): data = None self.set_icon('T', 'image', data) def getTopImagePath(self): return self._dummyPixmap def resetTopImagePath(self): pass def setBottomImagePath(self, data): if data.isNull(): data = None self.set_icon('B', 'image', data) def getBottomImagePath(self): return self._dummyPixmap def resetBottomImagePath(self): pass left_image_path = QtCore.pyqtProperty(QPixmap, getLeftImagePath, setLeftImagePath, resetLeftImagePath) right_image_path = QtCore.pyqtProperty(QPixmap, getRightImagePath, setRightImagePath, resetRightImagePath) center_image_path = QtCore.pyqtProperty(QPixmap, getCenterImagePath, setCenterImagePath, resetCenterImagePath) top_image_path = QtCore.pyqtProperty(QPixmap, getTopImagePath, setTopImagePath, resetTopImagePath) bottom_image_path = QtCore.pyqtProperty(QPixmap, getBottomImagePath, setBottomImagePath, resetBottomImagePath) def getFont(self): return self._font def setFont(self, value): self._font = value def resetFont(self): self._font = QFont('Lato Heavy', 20) button_font = QtCore.pyqtProperty(QFont, getFont, setFont, resetFont) def setLeftText(self, data): self._textL = data if not data.strip(): data = None self.set_icon('L', 'text', data) def getLeftText(self): return self._textL def resetLeftText(self): self._textL = '' self.set_icon('L', 'text', '') def setRightText(self, data): self._textR = data if not data.strip(): data = None self.set_icon('R', 'text', data) def getRightText(self): return self._textR def resetRightText(self): self._textR = '' self.set_icon('R', 'text', '') def setCenterText(self, data): self._textC = data if not data.strip(): data = None self.set_icon('C', 'text', data) def getCenterText(self): return self._textC def resetCenterText(self): self._textC = '' self.set_icon('C', 'text', '') def setTopText(self, data): self._textT = data if not data.strip(): data = None self.set_icon('T', 'text', data) def getTopText(self): return self._textT def resetTopText(self): self._textT = '' self.set_icon('T', 'text', '') def setBottomText(self, data): self._textB = data if not data.strip(): data = None self.set_icon('B', 'text', data) def getBottomText(self): return self._textB def resetBottomText(self): self._textB = '' self.set_icon('B', 'text', '') left_text = QtCore.pyqtProperty(str, getLeftText, setLeftText, resetLeftText) right_text = QtCore.pyqtProperty(str, getRightText, setRightText, resetRightText) center_text = QtCore.pyqtProperty(str, getCenterText, setCenterText, resetCenterText) top_text = QtCore.pyqtProperty(str, getTopText, setTopText, resetTopText) bottom_text = QtCore.pyqtProperty(str, getBottomText, setBottomText, resetBottomText) @QtCore.pyqtSlot(QColor) def set_true_color(self, color): self._true_color = color self.setLight(self.colorState) @QtCore.pyqtSlot(str) def set_true_color(self, color): self._true_color = QColor(color) self.setLight(self.colorState) def get_true_color(self): return self._true_color def reset_true_color(self): self._true_color = QColor('lawngreen') self.setLight(self.colorState) @QtCore.pyqtSlot(QColor) def set_false_color(self, color): self._false_color = color self.setLight(self.colorState) @QtCore.pyqtSlot(str) def set_false_color(self, color): self._false_color = QColor(color) self.setLight(self.colorState) def get_false_color(self): return self._false_color def reset_false_color(self): self._false_color = QColor('gray') self.setLight(self.colorState) def set_text_color(self, color): self._text_color = QColor(color) self.update() def get_text_color(self): return self._text_color def reset_text_color(self): self._text_color = QColor('white') self.update() true_color = QtCore.pyqtProperty(QColor, get_true_color, set_true_color, reset_true_color) false_color = QtCore.pyqtProperty(QColor, get_false_color, set_false_color, reset_false_color) text_color = QtCore.pyqtProperty(QColor, get_text_color, set_text_color, reset_text_color) @QtCore.pyqtSlot(str) def btn_pressed(self, btn): print("Button pressed", btn) @QtCore.pyqtSlot(str) def btn_released(self, btn): print("Button released", btn) # required code for object indexing def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value)
class SelectionTool(BaseTool): name = QApplication.translate("SelectionTool", "Selection") iconPath = ":cursor.svg" def __init__(self, parent=None): super().__init__(parent) self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self._shouldMove = False self._shouldPrepareUndo = False # helpers def _createAnchor(self, *args): widget = self.parent() glyph = self._glyph glyph.prepareUndo() pos = widget.mapToCanvas(widget.mapFromGlobal(self._cachedPos)) # remove template anchors for anchor in glyph.anchors: if anchor.name == "new anchor": glyph.removeAnchor(anchor) # add one at position anchor = glyph.instantiateAnchor() anchor.x = pos.x() anchor.y = pos.y() anchor.name = "new anchor" glyph.appendAnchor(anchor) def _createComponent(self, *args): widget = self.parent() glyph = self._glyph glyph.prepareUndo() newGlyph, ok = AddComponentDialog.getNewGlyph(widget, glyph) if ok and newGlyph is not None: component = glyph.instantiateComponent() component.baseGlyph = newGlyph.name glyph.appendComponent(component) def _createGuideline(self, *args): widget = self.parent() glyph = self._glyph glyph.prepareUndo() pos = widget.mapToCanvas(widget.mapFromGlobal(self._cachedPos)) content = dict(x=pos.x(), y=pos.y()) guideline = glyph.instantiateGuideline(content) glyph.appendGuideline(guideline) def _goToGlyph(self, glyphName): widget = self.parent() font = self._glyph.font if glyphName in font: glyph = font[glyphName] glyphWindow = GlyphWindow(glyph, widget.window().parent()) glyphWindow.show() def _toggleGuideline(self, guideline): glyph = self._glyph font = glyph.font if font is None: return if isinstance(guideline.getParent(), Glyph): glyph.removeGuideline(guideline) font.appendGuideline(guideline) else: font.removeGuideline(guideline) glyph.appendGuideline(guideline) def _getSelectedCandidatePoint(self): """ If there is exactly one point selected in the glyph, return it. Else return None. """ candidates = set() for contour in self._glyph: sel = contour.selection if len(sel) > 1: return None elif not len(sel): continue pt = next(iter(sel)) candidates.add((pt, contour)) if len(candidates) == 1: return next(iter(candidates)) return None def _getOffCurveSiblingPoint(self, contour, point): index = contour.index(point) for d in (-1, 1): sibling = contour.getPoint(index + d) if sibling.segmentType is not None: return sibling raise IndexError def _moveOnCurveAlongHandles(self, contour, pt, x, y): # TODO: offCurves if pt.segmentType is not None and pt.smooth and len(contour) >= 3: index = contour.index(pt) prevCP = contour.getPoint(index - 1) nextCP = contour.getPoint(index + 1) # we need at least one offCurve so that it makes sense # slide the onCurve around if prevCP.segmentType is None or nextCP.segmentType is None: projX, projY, _ = bezierMath.lineProjection( prevCP.x, prevCP.y, nextCP.x, nextCP.y, x, y, False) # short-circuit UIMove because we're only moving this point pt.x = projX pt.y = projY contour.dirty = True return True return False def _findSegmentUnderMouse(self, pos, action=None): scale = self.parent().inverseScale() for contour in self._glyph: for index, point in enumerate(contour): if point.segmentType == "line": prev = contour.getPoint(index-1) dist = bezierMath.lineDistance( prev.x, prev.y, point.x, point.y, pos.x(), pos.y()) # TODO: somewhat arbitrary if dist < 5 * scale: return [prev, point], contour elif point.segmentType in ("curve", "qcurve"): if action == "insert": continue if point.segmentType == "curve": bez = [contour.getPoint(index-3+i) for i in range(4)] else: bez = [point] i = 1 while i < 2 or point.segmentType is None: point = contour.getPoint(index-i) bez.append(point) i += 1 bez.reverse() if _pointWithinThreshold(pos.x(), pos.y(), bez, 5 * scale): return bez, contour return None def _performSegmentClick(self, pos, action=None, segmentTuple=None): if segmentTuple is None: segmentTuple = self._findSegmentUnderMouse(pos, action) if segmentTuple is None: return segment, contour = segmentTuple prev, point = segment[0], segment[-1] if point.segmentType == "line": if action == "insert": index = contour.index(point) self._glyph.prepareUndo() contour.holdNotifications() for i, t in enumerate((.35, .65)): xt = prev.x + t * (point.x - prev.x) yt = prev.y + t * (point.y - prev.y) contour.insertPoint( index+i, point.__class__((xt, yt))) point.segmentType = "curve" contour.releaseHeldNotifications() return elif point.segmentType not in ("curve", "qcurve"): return if action == "selectContour": contour.selected = not contour.selected self._shouldMove = self._shouldPrepareUndo = True def _maybeJoinContour(self, pos): def getAtEdge(contour, pt): for index in range(2): if contour[index-1] == pt: return index - 1 return None if self._itemTuple is None: return item, parent = self._itemTuple if parent is None or not (item.segmentType and parent.open): return ptIndex = getAtEdge(parent, item) if ptIndex is None: return widget = self.parent() items = widget.itemsAt(pos) for point, contour in zip(items["points"], items["contours"]): if point == item or not (point.segmentType and contour.open): continue otherIndex = getAtEdge(contour, point) if otherIndex is None: continue if parent != contour: # TODO: blacklist single onCurve contours # Note reverse uses different point objects # TODO: does it have to work this way? if not ptIndex: parent.reverse() if otherIndex == -1: contour.reverse() dragPoint = parent[-1] parent.removePoint(dragPoint) contour[0].segmentType = dragPoint.segmentType contour.drawPoints(parent) glyph = contour.glyph glyph.removeContour(contour) parent.dirty = True else: if item.segmentType == "move": item.x = point.x item.y = point.y contour.removePoint(point) else: contour.removePoint(item) contour[0].segmentType = "line" contour.dirty = True return def _moveForEvent(self, event): key = event.key() modifiers = event.modifiers() dx, dy = 0, 0 if key == Qt.Key_Left: dx = -1 elif key == Qt.Key_Up: dy = 1 elif key == Qt.Key_Right: dx = 1 elif key == Qt.Key_Down: dy = -1 if modifiers & Qt.ShiftModifier: dx *= 10 dy *= 10 if modifiers & Qt.ControlModifier: dx *= 10 dy *= 10 return (dx, dy) def _renameItem(self, item): widget = self.parent() newName, ok = RenameDialog.getNewName(widget, item.name) if ok: item.name = newName def _reverse(self, target=None): if target is None: selectedContours = set() for contour in self._glyph: if contour.selection: selectedContours.add(contour) target = selectedContours or self._glyph for contour in target: contour.reverse() def _setStartPoint(self, point, contour): index = contour.index(point) contour.setStartPoint(index) # actions def showContextMenu(self, event): widget = self.parent() self._cachedPos = event.globalPos() menu = QMenu(widget) itemTuple = widget.itemAt(event.localPos()) targetContour = None if itemTuple is not None: item, parent = itemTuple if parent is not None and item.segmentType: targetContour = [parent] menu.addAction(self.tr("Set Start Point"), lambda: self._setStartPoint(item, parent)) elif isinstance(item, Component): menu.addAction(self.tr("Go To Glyph"), lambda: self._goToGlyph(item.baseGlyph)) menu.addAction(self.tr("Decompose"), lambda: self._glyph.decomposeComponent(item)) menu.addAction(self.tr("Decompose All"), self._glyph.decomposeAllComponents) elif isinstance(item, Guideline): if isinstance(item.getParent(), Glyph): text = self.tr("Make Global") else: text = self.tr("Make Local") menu.addAction(text, lambda: self._toggleGuideline(item)) if targetContour is not None: reverseText = self.tr("Reverse Contour") else: # XXX: text and action shouldnt be decoupled if self._glyph.selection: reverseText = self.tr("Reverse Selected Contours") else: reverseText = self.tr("Reverse All Contours") menu.addAction(reverseText, lambda: self._reverse(targetContour)) menu.addSeparator() menu.addAction(self.tr("Add Component…"), self._createComponent) menu.addAction(self.tr("Add Anchor"), self._createAnchor) menu.addAction(self.tr("Add Guideline"), self._createGuideline) menu.exec_(self._cachedPos) self._cachedPos = None # events def keyPressEvent(self, event): key = event.key() if platformSpecific.isDeleteEvent(event): glyph = self._glyph # TODO: fuse more the two methods, they're similar and delete is # Cut except not putting in the clipboard if event.modifiers() & Qt.AltModifier: deleteUISelection(glyph) else: preserveShape = not event.modifiers() & Qt.ShiftModifier # TODO: prune glyph.prepareUndo() removeUIGlyphElements(glyph, preserveShape) elif key in arrowKeys: # TODO: prune self._glyph.prepareUndo() dx, dy = self._moveForEvent(event) # TODO: seems weird that glyph.selection and selected don't incl. # anchors and components while glyph.move does... see what glyphs # does moveUIGlyphElements(self._glyph, dx, dy) elif key in navKeys: pack = self._getSelectedCandidatePoint() if pack is not None: point, contour = pack point.selected = False index = contour.index(point) offset = int(key == Qt.Key_Greater) or -1 newPoint = contour.getPoint(index + offset) newPoint.selected = True contour.postNotification( notification="Contour.SelectionChanged") def mousePressEvent(self, event): if event.button() & Qt.RightButton: self.showContextMenu(event) return widget = self.parent() addToSelection = event.modifiers() & Qt.ControlModifier self._origin = self._prevPos = pos = self.magnetPos(event.localPos()) self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: itemUnderMouse, parentContour = self._itemTuple if not (itemUnderMouse.selected or addToSelection): unselectUIGlyphElements(self._glyph) itemUnderMouse.selected = True if parentContour is not None: parentContour.postNotification( notification="Contour.SelectionChanged") self._shouldPrepareUndo = True else: action = "insert" if event.modifiers() & Qt.AltModifier else None segmentTuple = self._findSegmentUnderMouse(pos, action) if segmentTuple is not None: segment, contour = segmentTuple selected = segment[0].selected and segment[-1].selected else: selected = False if not selected: if addToSelection: self._oldSelection = self._glyph.selection else: unselectUIGlyphElements(self._glyph) self._performSegmentClick(pos, action, segmentTuple) else: self._shouldMove = True widget.update() def mouseMoveEvent(self, event): canvasPos = event.localPos() glyph = self._glyph widget = self.parent() if self._shouldMove or self._itemTuple is not None: if self._shouldPrepareUndo: self._glyph.prepareUndo() self._shouldPrepareUndo = False modifiers = event.modifiers() if self._itemTuple is not None: # Alt: move point along handles if modifiers & Qt.AltModifier and len(glyph.selection) == 1: item, parent = self._itemTuple if parent is not None: x, y = canvasPos.x(), canvasPos.y() didMove = self._moveOnCurveAlongHandles( parent, item, x, y) if didMove: return # Shift: clamp pos on axis elif modifiers & Qt.ShiftModifier: item, parent = self._itemTuple if parent is not None: if item.segmentType is None: onCurve = self._getOffCurveSiblingPoint( parent, item) canvasPos = self.clampToOrigin( canvasPos, QPointF(onCurve.x, onCurve.y)) else: canvasPos = self.clampToOrigin( canvasPos, self._origin) dx = canvasPos.x() - self._prevPos.x() dy = canvasPos.y() - self._prevPos.y() moveUIGlyphElements(glyph, dx, dy) self._prevPos = canvasPos else: self._rubberBandRect = QRectF(self._origin, canvasPos).normalized() items = widget.items(self._rubberBandRect) points = set(items["points"]) if event.modifiers() & Qt.ControlModifier: points ^= self._oldSelection # TODO: fine-tune this more, maybe add optional args to items... if event.modifiers() & Qt.AltModifier: points = set(pt for pt in points if pt.segmentType) if points != self._glyph.selection: # TODO: doing this takes more time than by-contour # discrimination for large point count glyph.selection = points widget.update() def mouseReleaseEvent(self, event): self._maybeJoinContour(event.localPos()) self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self._shouldMove = False self.parent().update() def mouseDoubleClickEvent(self, event): widget = self.parent() self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: item, parent = self._itemTuple if parent is None: if isinstance(item, (Anchor, Guideline)): self._renameItem(item) else: point, contour = item, parent if point.segmentType is not None: self._glyph.prepareUndo() point.smooth = not point.smooth contour.dirty = True # if we have one offCurve, make it tangent maybeProjectUISmoothPointOffcurve(contour, point) else: self._performSegmentClick(event.localPos(), "selectContour") # custom painting def paint(self, painter): if self._rubberBandRect is None: return widget = self.parent() # okay, OS-native rubber band does not support painting with # floating-point coordinates # paint directly on the widget with unscaled context widgetOrigin = widget.mapFromCanvas(self._rubberBandRect.bottomLeft()) widgetMove = widget.mapFromCanvas(self._rubberBandRect.topRight()) option = QStyleOptionRubberBand() option.initFrom(widget) option.opaque = False option.rect = QRectF(widgetOrigin, widgetMove).toRect() option.shape = QRubberBand.Rectangle painter.save() painter.setRenderHint(QPainter.Antialiasing, False) painter.resetTransform() widget.style().drawControl( QStyle.CE_RubberBand, option, painter, widget) painter.restore()
class SelectionTool(BaseTool): name = QApplication.translate("SelectionTool", "Selection") iconPath = ":cursor.svg" def __init__(self, parent=None): super().__init__(parent) self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self._shouldMove = False self._shouldPrepareUndo = False # helpers def _createAnchor(self, *args): widget = self.parent() pos = widget.mapToCanvas(widget.mapFromGlobal(self._cachedPos)) newAnchorName, ok = AddAnchorDialog.getNewAnchorName(widget, pos) if ok: anchor = TAnchor() anchor.x = pos.x() anchor.y = pos.y() anchor.name = newAnchorName self._glyph.appendAnchor(anchor) def _createComponent(self, *args): widget = self.parent() newGlyph, ok = AddComponentDialog.getNewGlyph(widget, self._glyph) if ok and newGlyph is not None: component = TComponent() component.baseGlyph = newGlyph.name self._glyph.appendComponent(component) def _getSelectedCandidatePoint(self): """ If there is exactly one point selected in the glyph, return it. Else return None. """ candidates = set() for contour in self._glyph: sel = contour.selection if len(sel) > 1: return None elif not len(sel): continue pt = next(iter(sel)) candidates.add((pt, contour)) if len(candidates) == 1: return next(iter(candidates)) return None def _getOffCurveSiblingPoint(self, contour, point): index = contour.index(point) for d in (-1, 1): sibling = contour.getPoint(index + d) if sibling.segmentType is not None: return sibling raise IndexError def _moveOnCurveAlongHandles(self, contour, pt, x, y): # TODO: offCurves if pt.segmentType is not None and pt.smooth and len(contour) >= 3: index = contour.index(pt) prevCP = contour.getPoint(index - 1) nextCP = contour.getPoint(index + 1) # we need at least one offCurve so that it makes sense # slide the onCurve around if prevCP.segmentType is None or nextCP.segmentType is None: projX, projY, _ = bezierMath.lineProjection( prevCP.x, prevCP.y, nextCP.x, nextCP.y, x, y, False) # short-circuit UIMove because we're only moving this point pt.x = projX pt.y = projY contour.dirty = True return True return False def _computeLineClick(self, pos, insert=False): scale = self.parent().inverseScale() for contour in self._glyph: for index, point in enumerate(contour): if point.segmentType == "line": prev = contour.getPoint(index-1) dist = bezierMath.lineDistance( prev.x, prev.y, point.x, point.y, pos.x(), pos.y()) # TODO: somewhat arbitrary if dist < 5 * scale: if insert: self._glyph.prepareUndo() contour.holdNotifications() for i, t in enumerate((.35, .65)): xt = prev.x + t * (point.x - prev.x) yt = prev.y + t * (point.y - prev.y) contour.insertPoint( index+i, point.__class__((xt, yt))) point.segmentType = "curve" contour.releaseHeldNotifications() else: prev.selected = point.selected = True contour.postNotification( notification="Contour.SelectionChanged") self._shouldMove = self._shouldPrepareUndo = True return def _moveForEvent(self, event): key = event.key() modifiers = event.modifiers() dx, dy = 0, 0 if key == Qt.Key_Left: dx = -1 elif key == Qt.Key_Up: dy = 1 elif key == Qt.Key_Right: dx = 1 elif key == Qt.Key_Down: dy = -1 if modifiers & Qt.ShiftModifier: dx *= 10 dy *= 10 if modifiers & Qt.ControlModifier: dx *= 10 dy *= 10 return (dx, dy) def _renameAnchor(self, anchor): widget = self.parent() newAnchorName, ok = AddAnchorDialog.getNewAnchorName( widget, None, anchor.name) if ok: anchor.name = newAnchorName def _reverse(self): selectedContours = set() for contour in self._glyph: if contour.selection: selectedContours.add(contour) target = selectedContours or self._glyph for contour in target: contour.reverse() def _setStartPoint(self, point, contour): index = contour.index(point) contour.setStartPoint(index) # actions def showContextMenu(self, event): widget = self.parent() self._cachedPos = event.globalPos() menu = QMenu(widget) menu.addAction(self.tr("Add Anchor…"), self._createAnchor) menu.addAction(self.tr("Add Component…"), self._createComponent) menu.addAction(self.tr("Reverse"), self._reverse) itemTuple = widget.itemAt(event.localPos()) if itemTuple is not None: item, parent = itemTuple if parent is not None and item.segmentType: menu.addSeparator() menu.addAction(self.tr("Set Start Point"), lambda: self._setStartPoint(item, parent)) menu.exec_(self._cachedPos) self._cachedPos = None # events def keyPressEvent(self, event): key = event.key() if event.matches(QKeySequence.Delete): glyph = self._glyph # TODO: prune glyph.prepareUndo() preserveShape = not event.modifiers() & Qt.ShiftModifier for anchor in glyph.anchors: if anchor.selected: glyph.removeAnchor(anchor) for contour in reversed(glyph): removeUISelection(contour, preserveShape) for component in glyph.components: if component.selected: glyph.removeComponent(component) if glyph.image.selected: glyph.image = None elif key in arrowKeys: # TODO: prune self._glyph.prepareUndo() delta = self._moveForEvent(event) # TODO: seems weird that glyph.selection and selected don't incl. # anchors and components while glyph.move does... see what glyphs # does hadSelection = False for anchor in self._glyph.anchors: if anchor.selected: anchor.move(delta) hadSelection = True for contour in self._glyph: moveUISelection(contour, delta) # XXX: shouldn't have to recalc this if contour.selection: hadSelection = True for component in self._glyph.components: if component.selected: component.move(delta) hadSelection = True image = self._glyph.image if image.selected: image.move(delta) hadSelection = True if not hadSelection: event.ignore() elif key in navKeys: pack = self._getSelectedCandidatePoint() if pack is not None: point, contour = pack point.selected = False index = contour.index(point) offset = int(key == Qt.Key_Greater) or -1 newPoint = contour.getPoint(index + offset) newPoint.selected = True contour.postNotification( notification="Contour.SelectionChanged") def mousePressEvent(self, event): if event.button() & Qt.RightButton: self.showContextMenu(event) return widget = self.parent() addToSelection = event.modifiers() & Qt.ControlModifier self._origin = self.magnetPos(event.localPos()) self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: itemUnderMouse, parentContour = self._itemTuple if not (itemUnderMouse.selected or addToSelection): for anchor in self._glyph.anchors: anchor.selected = False for component in self._glyph.components: component.selected = False self._glyph.selected = False self._glyph.image.selected = False itemUnderMouse.selected = True if parentContour is not None: parentContour.postNotification( notification="Contour.SelectionChanged") self._shouldPrepareUndo = True else: if addToSelection: self._oldSelection = self._glyph.selection else: for anchor in self._glyph.anchors: anchor.selected = False for component in self._glyph.components: component.selected = False self._glyph.selected = False self._glyph.image.selected = False self._computeLineClick(event.localPos()) widget.update() def mouseMoveEvent(self, event): canvasPos = event.localPos() widget = self.parent() if self._shouldMove or self._itemTuple is not None: if self._shouldPrepareUndo: self._glyph.prepareUndo() self._shouldPrepareUndo = False modifiers = event.modifiers() # Alt: move point along handles if modifiers & Qt.AltModifier and len(self._glyph.selection) == 1: item, parent = self._itemTuple if parent is not None: x, y = canvasPos.x(), canvasPos.y() didMove = self._moveOnCurveAlongHandles(parent, item, x, y) if didMove: return # Shift: clamp pos on axis elif modifiers & Qt.ShiftModifier: item, parent = self._itemTuple if parent is not None: if item.segmentType is None: onCurve = self._getOffCurveSiblingPoint(parent, item) canvasPos = self.clampToOrigin( canvasPos, QPointF(onCurve.x, onCurve.y)) dx = canvasPos.x() - self._origin.x() dy = canvasPos.y() - self._origin.y() for anchor in self._glyph.anchors: if anchor.selected: anchor.move((dx, dy)) for contour in self._glyph: moveUISelection(contour, (dx, dy)) for component in self._glyph.components: if component.selected: component.move((dx, dy)) image = self._glyph.image if image.selected: image.move((dx, dy)) self._origin = canvasPos else: self._rubberBandRect = QRectF(self._origin, canvasPos).normalized() items = widget.items(self._rubberBandRect) points = set(items["points"]) if event.modifiers() & Qt.ControlModifier: points ^= self._oldSelection # TODO: fine-tune this more, maybe add optional args to items... if event.modifiers() & Qt.AltModifier: points = set(pt for pt in points if pt.segmentType) if points != self._glyph.selection: # TODO: doing this takes more time than by-contour # discrimination for large point count self._glyph.selection = points widget.update() def mouseReleaseEvent(self, event): self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self._shouldMove = False self.parent().update() def mouseDoubleClickEvent(self, event): widget = self.parent() self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: item, parent = self._itemTuple if parent is None: if isinstance(item, TAnchor): self._renameAnchor(item) else: point, contour = item, parent if point.segmentType is not None: self._glyph.prepareUndo() point.smooth = not point.smooth contour.dirty = True else: self._computeLineClick(event.localPos(), True) # custom painting def paint(self, painter): if self._rubberBandRect is None: return widget = self.parent() # okay, OS-native rubber band does not support painting with # floating-point coordinates # paint directly on the widget with unscaled context widgetOrigin = widget.mapFromCanvas(self._rubberBandRect.bottomLeft()) widgetMove = widget.mapFromCanvas(self._rubberBandRect.topRight()) option = QStyleOptionRubberBand() option.initFrom(widget) option.opaque = False option.rect = QRectF(widgetOrigin, widgetMove).toRect() option.shape = QRubberBand.Rectangle painter.save() painter.setRenderHint(QPainter.Antialiasing, False) painter.setTransform(QTransform()) widget.style().drawControl( QStyle.CE_RubberBand, option, painter, widget) painter.restore()
class RoundRectItem(QGraphicsObject): """Base class for most graphic objects in our scene""" def __init__(self, bounds, color=None, parent=None): """ Args: bounds - QRectF, geometry of object color - QColor or None parent - widget to contain this graphic item or None """ super(RoundRectItem, self).__init__(parent) self._fillRect = False self._bounds = QRectF(bounds) self._pix = QPixmap() self._color = color self.setCacheMode(QGraphicsItem.ItemCoordinateCache) def setFill(self, fill: bool): """ Changes the property of how the cell is filled. Args: fill: bool """ self._fillRect = fill self.update() @property def _gradient(self): gradient = QLinearGradient() gradient.setStart( (self._bounds.topLeft() + self._bounds.topRight()) / 2) gradient.setFinalStop( (self._bounds.bottomLeft() + self._bounds.bottomRight()) / 2) gradient.setColorAt(0, self._color) gradient.setColorAt(1, self._color.darker(200)) return gradient def paint(self, painter, option, widget): """Standard Qt paint event.""" if self._color: painter.setPen(Qt.NoPen) painter.setBrush(QColor(0, 0, 0, 64)) painter.drawRoundedRect(self._bounds.translated(2, 2), 25.0, 25.0) if self._fillRect: painter.setBrush(QApplication.palette().brush(QPalette.Window)) else: painter.setBrush(self._gradient) painter.setPen(QPen(Qt.black, 1)) painter.drawRoundedRect(self._bounds, 25.0, 25.0) if not self._pix.isNull(): if self._rounded_pixmap: painter.setRenderHint(QPainter.Antialiasing, True) brush = QBrush( self._pix.scaled(self._bounds.width(), self._bounds.height())) painter.setBrush(brush) painter.drawRoundedRect(self._bounds, 25.0, 25.0) else: painter.scale(self._bounds.width() / self._pix.width(), self._bounds.height() / self._pix.height()) painter.drawPixmap(-self._pix.width() / 2, -self._pix.height() / 2, self._pix) def boundingRect(self): """returns bounding rectangle""" return self._bounds.adjusted(0, 0, 2, 2) def setPixmap(self, pixmap_path: str, rounded_pixmap=False): """ Sets new pixmap for this graphic object. Args: pixmap_path: path to image for pixmap rounded_pixmap: make the picture rounded (used, e.g., for lava in the cells) """ self._rounded_pixmap = rounded_pixmap self._pix = QPixmap(pixmap_path) self.update()
class SelectionTool(BaseTool): name = QApplication.translate("SelectionTool", "Selection") iconPath = ":/resources/cursor.svg" def __init__(self, parent=None): super().__init__(parent) self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self._shouldPrepareUndo = False # helpers def _createAnchor(self, *args): widget = self.parent() pos = widget.mapToCanvas(widget.mapFromGlobal(self._cachedPos)) newAnchorName, ok = AddAnchorDialog.getNewAnchorName(widget, pos) if ok: anchor = TAnchor() anchor.x = pos.x() anchor.y = pos.y() anchor.name = newAnchorName self._glyph.appendAnchor(anchor) def _createComponent(self, *args): widget = self.parent() newGlyph, ok = AddComponentDialog.getNewGlyph(widget, self._glyph) if ok and newGlyph is not None: component = TComponent() component.baseGlyph = newGlyph.name self._glyph.appendComponent(component) def _getSelectedCandidatePoint(self): """ If there is exactly one point selected in the glyph, return it. Else return None. """ candidates = set() for contour in self._glyph: sel = contour.selection if len(sel) > 1: return None elif not len(sel): continue pt = next(iter(sel)) candidates.add((pt, contour)) if len(candidates) == 1: return next(iter(candidates)) return None def _getOffCurveSiblingPoint(self, contour, point): index = contour.index(point) for d in (-1, 1): sibling = contour.getPoint(index + d) if sibling.segmentType is not None: return sibling raise IndexError def _moveOnCurveAlongHandles(self, contour, pt, x, y): # TODO: offCurves if pt.segmentType is not None and pt.smooth and len(contour) >= 3: index = contour.index(pt) prevCP = contour.getPoint(index - 1) nextCP = contour.getPoint(index + 1) # we need at least one offCurve so that it makes sense # slide the onCurve around if prevCP.segmentType is None or nextCP.segmentType is None: projX, projY = bezierMath.lineProjection( prevCP.x, prevCP.y, nextCP.x, nextCP.y, x, y, False) # short-circuit UIMove because we're only moving this point pt.x = projX pt.y = projY contour.dirty = True return True return False def _moveForEvent(self, event): key = event.key() modifiers = event.modifiers() dx, dy = 0, 0 if key == Qt.Key_Left: dx = -1 elif key == Qt.Key_Up: dy = 1 elif key == Qt.Key_Right: dx = 1 elif key == Qt.Key_Down: dy = -1 if modifiers & Qt.ShiftModifier: dx *= 10 dy *= 10 if modifiers & Qt.ControlModifier: dx *= 10 dy *= 10 return (dx, dy) def _renameAnchor(self, anchor): widget = self.parent() newAnchorName, ok = AddAnchorDialog.getNewAnchorName( widget, None, anchor.name) if ok: anchor.name = newAnchorName def _reverse(self): selectedContours = set() for contour in self._glyph: if contour.selection: selectedContours.add(contour) target = selectedContours or self._glyph for contour in target: contour.reverse() def _setStartPoint(self, point, contour): index = contour.index(point) contour.setStartPoint(index) # actions def showContextMenu(self, event): widget = self.parent() self._cachedPos = event.globalPos() menu = QMenu(widget) menu.addAction(self.tr("Add Anchor…"), self._createAnchor) menu.addAction(self.tr("Add Component…"), self._createComponent) menu.addAction(self.tr("Reverse"), self._reverse) itemTuple = widget.itemAt(event.localPos()) if itemTuple is not None: item, parent = itemTuple if parent is not None and item.segmentType: menu.addSeparator() menu.addAction(self.tr("Set Start Point"), lambda: self._setStartPoint(item, parent)) menu.exec_(self._cachedPos) self._cachedPos = None # events def keyPressEvent(self, event): key = event.key() if key == platformSpecific.deleteKey: glyph = self._glyph # TODO: prune glyph.prepareUndo() preserveShape = not event.modifiers() & Qt.ShiftModifier for anchor in glyph.anchors: if anchor.selected: glyph.removeAnchor(anchor) for contour in reversed(glyph): removeUISelection(contour, preserveShape) for component in glyph.components: if component.selected: glyph.removeComponent(component) elif key in arrowKeys: # TODO: prune self._glyph.prepareUndo() delta = self._moveForEvent(event) # TODO: seems weird that glyph.selection and selected don't incl. # anchors and components while glyph.move does... see what glyphs # does hadSelection = False for anchor in self._glyph.anchors: if anchor.selected: anchor.move(delta) hadSelection = True for contour in self._glyph: moveUISelection(contour, delta) # XXX: shouldn't have to recalc this if contour.selection: hadSelection = True for component in self._glyph.components: if component.selected: component.move(delta) hadSelection = True if not hadSelection: event.ignore() elif key in navKeys: pack = self._getSelectedCandidatePoint() if pack is not None: point, contour = pack point.selected = False index = contour.index(point) offset = int(key == Qt.Key_Greater) or -1 newPoint = contour.getPoint(index + offset) newPoint.selected = True contour.postNotification( notification="Contour.SelectionChanged") def mousePressEvent(self, event): if event.button() & Qt.RightButton: self.showContextMenu(event) return widget = self.parent() addToSelection = event.modifiers() & Qt.ControlModifier self._origin = self.magnetPos(event.localPos()) self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: itemUnderMouse, parentContour = self._itemTuple if not (itemUnderMouse.selected or addToSelection): for anchor in self._glyph.anchors: anchor.selected = False for component in self._glyph.components: component.selected = False self._glyph.selected = False itemUnderMouse.selected = True if parentContour is not None: parentContour.postNotification( notification="Contour.SelectionChanged") self._shouldPrepareUndo = True else: if addToSelection: self._oldSelection = self._glyph.selection else: for anchor in self._glyph.anchors: anchor.selected = False for component in self._glyph.components: component.selected = False self._glyph.selected = False widget.update() def mouseMoveEvent(self, event): canvasPos = event.localPos() widget = self.parent() if self._itemTuple is not None: if self._shouldPrepareUndo: self._glyph.prepareUndo() self._shouldPrepareUndo = False modifiers = event.modifiers() # Alt: move point along handles if modifiers & Qt.AltModifier and len(self._glyph.selection) == 1: item, parent = self._itemTuple if parent is not None: x, y = canvasPos.x(), canvasPos.y() didMove = self._moveOnCurveAlongHandles(parent, item, x, y) if didMove: return # Shift: clamp pos on axis elif modifiers & Qt.ShiftModifier: item, parent = self._itemTuple if parent is not None: if item.segmentType is None: onCurve = self._getOffCurveSiblingPoint(parent, item) canvasPos = self.clampToOrigin( canvasPos, QPointF(onCurve.x, onCurve.y)) dx = canvasPos.x() - self._origin.x() dy = canvasPos.y() - self._origin.y() for anchor in self._glyph.anchors: if anchor.selected: anchor.move((dx, dy)) for contour in self._glyph: moveUISelection(contour, (dx, dy)) for component in self._glyph.components: if component.selected: component.move((dx, dy)) self._origin = canvasPos else: self._rubberBandRect = QRectF(self._origin, canvasPos).normalized() items = widget.items(self._rubberBandRect) points = set(items["points"]) if event.modifiers() & Qt.ControlModifier: points ^= self._oldSelection # TODO: fine-tune this more, maybe add optional args to items... if event.modifiers() & Qt.AltModifier: points = set(pt for pt in points if pt.segmentType) if points != self._glyph.selection: # TODO: doing this takes more time than by-contour # discrimination for large point count self._glyph.selection = points widget.update() def mouseReleaseEvent(self, event): self._itemTuple = None self._oldSelection = set() self._rubberBandRect = None self.parent().update() def mouseDoubleClickEvent(self, event): widget = self.parent() self._itemTuple = widget.itemAt(self._origin) if self._itemTuple is not None: item, parent = self._itemTuple if parent is None: if isinstance(item, TAnchor): self._renameAnchor(item) else: point, contour = item, parent if point.segmentType is not None: self._glyph.prepareUndo() point.smooth = not point.smooth contour.dirty = True # custom painting def paint(self, painter): if self._rubberBandRect is None: return widget = self.parent() # okay, OS-native rubber band does not support painting with # floating-point coordinates # paint directly on the widget with unscaled context widgetOrigin = widget.mapToWidget(self._rubberBandRect.bottomLeft()) widgetMove = widget.mapToWidget(self._rubberBandRect.topRight()) option = QStyleOptionRubberBand() option.initFrom(widget) option.opaque = False option.rect = QRectF(widgetOrigin, widgetMove).toRect() option.shape = QRubberBand.Rectangle painter.save() painter.setRenderHint(QPainter.Antialiasing, False) painter.setTransform(QTransform()) widget.style().drawControl(QStyle.CE_RubberBand, option, painter, widget) painter.restore()
class TestPathFinding(TestCase): ################################################################################ def setUp(self): """ """ # --left --right # ' ' # I | II | III # ------+==========+------ --top # VIII | IX (in) | IV # ------+==========+------ --bottom # VII | VI | V self._offset = 10 self._rect = QRectF(QPointF(-20, -10), QPointF(20, 10)) self._pointI = QPointF(self._rect.left()-self._offset, self._rect.top()-self._offset) self._pointII = QPointF(self._rect.center().x(), self._rect.top()-self._offset) self._pointIII = QPointF(self._rect.right()+ self._offset, self._rect.top()- self._offset) self._pointIV = QPointF(self._rect.right()+self._offset, self._rect.center().y()) self._pointV = QPointF(self._rect.right()+self._offset, self._rect.bottom()+self._offset) self._pointVI = QPointF(self._rect.center().x(), self._rect.bottom()+self._offset) self._pointVII = QPointF(self._rect.left()-self._offset, self._rect.bottom()+self._offset) self._pointVIII = QPointF(self._rect.left()-self._offset, self._rect.center().y()) self._pointIX = self._rect.center() self._lineI_VII = QLineF(self._pointI, self._pointVII) self._lineI_V = QLineF(self._pointI, self._pointV) self._lineII = QLineF(self._pointII, QPointF(self._rect.center().x(), self._rect.top())) self._lineII_IV = QLineF(QPointF(self._rect.right()-self._offset, self._rect.top()-self._offset), QPointF(self._rect.right()+self._offset, self._rect.top()+self._offset)) ################################################################################ def testPointRectDist(self): """ """ lineI = PathFinding.pointRectDist(self._pointI, self._rect) self.assertEqual(lineI.p2(), self._rect.topLeft()) self.assertEqual(lineI.length(), pow(pow(self._offset, 2)+pow(self._offset, 2), 0.5)) lineII = PathFinding.pointRectDist(self._pointII, self._rect) self.assertEqual(lineII.p2(), QPointF(self._rect.center().x(), self._rect.top())) self.assertEqual(lineII.length(), self._offset) lineIII = PathFinding.pointRectDist(self._pointIII, self._rect) self.assertEqual(lineIII.p2(), self._rect.topRight()) self.assertEqual(lineIII.length(), pow(pow(self._offset, 2)+pow(self._offset, 2), 0.5)) lineIV = PathFinding.pointRectDist(self._pointIV, self._rect) self.assertEqual(lineIV.p2(), QPointF(self._rect.right(), self._rect.center().y())) self.assertEqual(lineIV.length(), self._offset) lineV = PathFinding.pointRectDist(self._pointV, self._rect) self.assertEqual(lineV.p2(), self._rect.bottomRight()) self.assertEqual(lineV.length(), pow(pow(self._offset, 2)+pow(self._offset, 2), 0.5)) lineVI = PathFinding.pointRectDist(self._pointVI, self._rect) self.assertEqual(lineVI.p2(), QPointF(self._rect.center().x(), self._rect.bottom())) self.assertEqual(lineVI.length(), self._offset) lineVII = PathFinding.pointRectDist(self._pointVII, self._rect) self.assertEqual(lineVII.p2(), self._rect.bottomLeft()) self.assertEqual(lineVII.length(), pow(pow(self._offset, 2)+pow(self._offset, 2), 0.5)) lineVIII = PathFinding.pointRectDist(self._pointVIII, self._rect) self.assertEqual(lineVIII.p2(), QPointF(self._rect.left(), self._rect.center().y())) self.assertEqual(lineVIII.length(), self._offset) lineIX = PathFinding.pointRectDist(self._pointIX, self._rect) self.assertEqual(lineIX.p2(), self._pointIX) self.assertEqual(lineIX.length(), 0) ################################################################################ def testIntersects(self): """ """ rect = QRectF(QPointF(-50, -10), QPointF(50, 10)) # line completely outside of the rectangle self.assertFalse(PathFinding.intersects(rect, QLineF(QPointF(-100, -50), QPointF(-100, 50)))) # the line is a top side of the rectangle self.assertFalse(PathFinding.intersects(rect, QLineF(rect.topLeft(), rect.topRight()))) # the line starts at the left corner of the rectangle and is not perpendicular to any of the rectangle sides; # the line ends outside of the rectangle, not going through it self.assertFalse(PathFinding.intersects(rect, QLineF(rect.topLeft(), QPointF(-100, -100)))) # the line starts at the left corner of the rectangle and is perpendicular to the top side of the rectangle; # the line ends outside of the rectangle, not going through it self.assertFalse(PathFinding.intersects(rect, QLineF(rect.topLeft(), QPointF(rect.left(), rect.top() - 100)))) # the line is horizontal and goes straight through the center of the rectangle self.assertTrue(PathFinding.intersects(rect, QLineF(QPointF(-100, 0), QPointF(100, 0)))) # the line is vertical and goes straight through the center of the rectangle self.assertTrue(PathFinding.intersects(rect, QLineF(QPointF(0, -100), QPointF(0, 100)))) # the line is vertical and goes up from the bottom right corner of the rectangle self.assertFalse(PathFinding.intersects(rect, QLineF(rect.bottomRight(), QPointF(rect.right(), rect.top()-100)))) # the line is diagonal of the rectangle self.assertTrue(PathFinding.intersects(rect, QLineF(rect.topLeft(), rect.bottomRight())))