class GraphicsTextWidget(QGraphicsWidget): def __init__(self, text, parent=None, **kwargs): super().__init__(parent, **kwargs) self.labelItem = QGraphicsTextItem(self) self.setHtml(text) self.labelItem.document().documentLayout().documentSizeChanged.connect( self.onLayoutChanged ) def setGeometry(self, rect): self.prepareGeometryChange() super().setGeometry(rect) def onLayoutChanged(self, *args): self.updateGeometry() def sizeHint(self, which, constraint=QSizeF()): if which == Qt.MinimumSize: return self.labelItem.boundingRect().size() else: return self.labelItem.boundingRect().size() def setTextWidth(self, width): self.labelItem.setTextWidth(width) def setHtml(self, text): self.labelItem.setHtml(text)
def scientific_name_face(node, *args, **kwargs): scientific_name_text = QGraphicsTextItem() #underscore = node.name.replace("_", " ") words = node.name.split("_") text = [] if len(words) < 2: # some sort of acronym or bin name, leave it alone text = words elif len(words) > 2: if len(words) >= 5: text.extend( ['<b>' + words[0] + ' <i> ' + words[1], words[2] + ' </i> ']) text.extend(words[3:] + ['</b>']) elif len(words) == 3: text.extend([ ' <span style="color:grey"><i> ' + words[0], words[1] + words[2] + ' </i></span>' ]) else: # assume that everything after the # second word is strain name # which should not get italicized text.extend([ ' <span style="color:grey"><i> ' + words[0], words[1] + ' </i></span>' ]) text.extend(words[2:]) else: text.extend([ ' <span style="color:grey"><i> ' + words[0], words[1] + ' </i></span> ' ]) scientific_name_text.setHtml(' '.join(text)) # below is a bit of a hack - I've found that the height of the bounding # box gives a bit too much padding around the name, so I just minus 10 # from the height and recenter it. Don't know whether this is a generally # applicable number to use #myFont = QFont() masterItem = QGraphicsRectItem( 0, 0, scientific_name_text.boundingRect().width(), scientific_name_text.boundingRect().height() - 10) scientific_name_text.setParentItem(masterItem) center = masterItem.boundingRect().center() scientific_name_text.setPos( masterItem.boundingRect().x(), center.y() - scientific_name_text.boundingRect().height() / 2) # I don't want a border around the masterItem masterItem.setPen(QPen(QtCore.Qt.NoPen)) return masterItem
def qt_test(): """ Test that Qt actually works """ scene = QGraphicsScene() text = QGraphicsTextItem(None, scene) text.setHtml("<h2 align=\"center\">hello</h2><h2 align=\"center\">world 12334345354444444444444444444444444</h2>123"); text.setPos(QPointF(25,25)) view = QGraphicsView(scene) view.show() sys.exit(app.exec_())
def scientific_name_face(node, *args, **kwargs): scientific_name_text = QGraphicsTextItem() words = node.visual_label.split() text = [] if hasattr(node, 'bg_col'): container_div = '<div style="background-color:{};">'.format(node.bgcolor) text.append(container_div) if len(words) < 2: # some sort of acronym or bin name, leave it alone text.extend(words) elif len(words) > 2: if words[0] == 'Candidatus': # for candidatus names, only the Candidatus part is italicised # name shortening it for brevity text.append('<i>Ca.</i>') text.extend(words[1:]) elif re.match('^[A-Z]+$', words[0]): # If the first word is in all caps then it is an abreviation # so we don't want to italicize that at all text.extend(words) else: # assume that everything after the second word is strain name # which should not get italicised text.extend(['<i>'+words[0],words[1]+'</i>']) text.extend(words[2:]) else: text.extend(['<i>'+words[0],words[1]+'</i>']) if hasattr(node, 'bg_col'): text.append('</div>') scientific_name_text.setHtml(' '.join(text)) #print(scientific_name_text.boundingRect().width(), scientific_name_text.boundingRect().height()) # below is a bit of a hack - I've found that the height of the bounding # box gives a bit too much padding around the name, so I just minus 10 # from the height and recenter it. Don't know whether this is a generally # applicable number to use masterItem = QGraphicsRectItem(0, 0, scientific_name_text.boundingRect().width(), scientific_name_text.boundingRect().height() - 10) scientific_name_text.setParentItem(masterItem) center = masterItem.boundingRect().center() scientific_name_text.setPos(masterItem.boundingRect().x(), center.y() - scientific_name_text.boundingRect().height()/2) # I dont want a border around the masterItem masterItem.setPen(QPen(QtCore.Qt.NoPen)) return masterItem
def trinomial_face(binom, spp, ftype="Verdana", fsize=10, fgcolor="black", fstyle="normal", bold=False): """ Stolen from: https://connorskennerton.wordpress.com/2016/11/16/python-ete3-formatting-organism-names-the-way-i-want/ """ font = QFont(ftype, fsize) font.setBold(bold) if fstyle == "italic": font.setStyle(QFont.StyleItalic) elif fstyle == "oblique": font.setStyle(QFont.StyleOblique) text = QGraphicsTextItem() text.setFont(font) if spp is None: text.setHtml("<i>{}</i>".format(binom)) else: text.setHtml("<i>{}</i> {}".format(binom, spp)) rect = QGraphicsRectItem(0, 0, text.boundingRect().width(), text.boundingRect().height() - 10) text.setParentItem(rect) center = rect.boundingRect().center() text.setPos(rect.boundingRect().x(), center.y() - text.boundingRect().height() / 2) # no border rect.setPen(QPen(QtCore.Qt.NoPen)) return ete3.faces.StaticItemFace(rect)
class ItemUsuario(ItemMovel): def __init__(self, usuario, rect=QRectF(0, 0, 100, 50), parent=None): super().__init__(False, True, rect, parent) self._numConversasNaoVisualizadas = 0 self.selected = False self._textItem = QGraphicsTextItem(self) self._textItem.setFont(self.fontUser()) self._textItem.setPos(0, 10) self.setaImg = ResourceUtil.getImage('images/right.png').scaled(80, 40, aspectRatioMode=Qt.KeepAspectRatio) self.topoImg = ResourceUtil.getImage('images/fundo/topo.png') self.background = ResourceUtil.getImage('images/postit3.png') self.setUsuario(usuario) def setUsuario(self, usuario): self._usuario = usuario self.atualizaTexto() def getUsuario(self): return self._usuario def setNumConversasNaoVisualizadas(self, num): self._numConversasNaoVisualizadas = num self.atualizaTexto() def atualizaTexto(self): texto = "<center>{0} - {1}".format(self._usuario.getNome(), self._usuario.getIP()) if self._numConversasNaoVisualizadas > 0: texto += " <b>({0})</b>".format(self._numConversasNaoVisualizadas) texto += "</center>" self._textItem.setHtml(texto) def setRect(self, rect): super().setRect(rect) self._textItem.setTextWidth(rect.size().width() - self.setaImg.rect().width()) def paint(self, painter, widget, option): painter.setPen(Qt.NoPen) if self.selected: rect = self.boundingRect() rectImg = self.setaImg.rect() painter.drawImage(rectImg.adjusted(rect.width() - rectImg.width(), 0, rect.width() - rectImg.width(), 0), self.setaImg) painter.drawImage(self.boundingRect(), self.background) """if self.isUnderMouse(): painter.drawImage(self.boundingRect().adjusted(0,10,0,0), self.topoImg) painter.save() painter.setPen(Qt.NoPen) st = Status.getInstance(self.getUsuario().getStatus()) if st == Status.ONLINE: painter.setBrush(QColor(0,255,0,50)) elif st == Status.OCUPADO: painter.setBrush(QColor(255,0,0,50)) elif st == Status.AUSENTE: painter.setBrush(QColor(240, 226, 31, 50)) painter.drawRoundedRect(self.getRect(), 7, 5) painter.restore()""" super().paint(painter, widget, option) def fontUser(self): font = QFont("LoveYaLikeASister") font.setBold(True) return font
class NodeItem(QGraphicsObject): """ An widget node item in the canvas. """ #: Signal emitted when the scene position of the node has changed. positionChanged = Signal() #: Signal emitted when the geometry of the channel anchors changes. anchorGeometryChanged = Signal() #: Signal emitted when the item has been activated (by a mouse double #: click or a keyboard) activated = Signal() #: The item is under the mouse. hovered = Signal() #: Span of the anchor in degrees ANCHOR_SPAN_ANGLE = 90 #: Z value of the item Z_VALUE = 100 def __init__(self, widget_description=None, parent=None, **kwargs): QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QGraphicsItem.ItemHasNoContents, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) # central body shape item self.shapeItem = None # in/output anchor items self.inputAnchorItem = None self.outputAnchorItem = None # title text item self.captionTextItem = None # error, warning, info items self.errorItem = None self.warningItem = None self.infoItem = None self.__title = "" self.__processingState = 0 self.__progress = -1 self.__error = None self.__warning = None self.__info = None self.__anchorLayout = None self.__animationEnabled = False self.setZValue(self.Z_VALUE) self.setupGraphics() self.setWidgetDescription(widget_description) @classmethod def from_node(cls, node): """ Create an :class:`NodeItem` instance and initialize it from a :class:`SchemeNode` instance. """ self = cls() self.setWidgetDescription(node.description) # self.setCategoryDescription(node.category) return self @classmethod def from_node_meta(cls, meta_description): """ Create an `NodeItem` instance from a node meta description. """ self = cls() self.setWidgetDescription(meta_description) return self def setupGraphics(self): """ Set up the graphics. """ shape_rect = QRectF(-24, -24, 48, 48) self.shapeItem = NodeBodyItem(self) self.shapeItem.setShapeRect(shape_rect) self.shapeItem.setAnimationEnabled(self.__animationEnabled) # Rect for widget's 'ears'. anchor_rect = QRectF(-31, -31, 62, 62) self.inputAnchorItem = SinkAnchorItem(self) input_path = QPainterPath() start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 input_path.arcMoveTo(anchor_rect, start_angle) input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) self.inputAnchorItem.setAnchorPath(input_path) self.outputAnchorItem = SourceAnchorItem(self) output_path = QPainterPath() start_angle = self.ANCHOR_SPAN_ANGLE / 2 output_path.arcMoveTo(anchor_rect, start_angle) output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE) self.outputAnchorItem.setAnchorPath(output_path) self.inputAnchorItem.hide() self.outputAnchorItem.hide() # Title caption item self.captionTextItem = QGraphicsTextItem(self) self.captionTextItem.setPlainText("") self.captionTextItem.setPos(0, 33) def iconItem(standard_pixmap): item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), iconSize=QSize(16, 16)) item.hide() return item self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) # TODO: Remove the set[Widget|Category]Description. The user should # handle setting of icons, title, ... def setWidgetDescription(self, desc): """ Set widget description. """ self.widget_description = desc if desc is None: return icon = icon_loader.from_description(desc).get(desc.icon) if icon: self.setIcon(icon) if not self.title(): self.setTitle(desc.name) if desc.inputs: self.inputAnchorItem.show() if desc.outputs: self.outputAnchorItem.show() tooltip = NodeItem_toolTipHelper(self) self.setToolTip(tooltip) def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color) def setIcon(self, icon): """ Set the node item's icon (:class:`QIcon`). """ if isinstance(icon, QIcon): self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, iconSize=QSize(36, 36)) self.icon_item.setPos(-18, -18) else: raise TypeError def setColor(self, color, selectedColor=None): """ Set the widget color. """ if selectedColor is None: selectedColor = saturated(color, 150) palette = create_palette(color, selectedColor) self.shapeItem.setPalette(palette) def setPalette(self, palette): # TODO: The palette should override the `setColor` raise NotImplementedError def setTitle(self, title): """ Set the node title. The title text is displayed at the bottom of the node. """ self.__title = title self.__updateTitleText() def title(self): """ Return the node title. """ return self.__title title_ = Property(unicode, fget=title, fset=setTitle, doc="Node title text.") def setFont(self, font): """ Set the title text font (:class:`QFont`). """ if font != self.font(): self.prepareGeometryChange() self.captionTextItem.setFont(font) self.__updateTitleText() def font(self): """ Return the title text font. """ return self.captionTextItem.font() def setAnimationEnabled(self, enabled): """ Set the node animation enabled state. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled self.shapeItem.setAnimationEnabled(enabled) def animationEnabled(self): """ Are node animations enabled. """ return self.__animationEnabled def setProcessingState(self, state): """ Set the node processing state i.e. the node is processing (is busy) or is idle. """ if self.__processingState != state: self.__processingState = state self.shapeItem.setProcessingState(state) if not state: # Clear the progress meter. self.setProgress(-1) if self.__animationEnabled: self.shapeItem.ping() def processingState(self): """ The node processing state. """ return self.__processingState processingState_ = Property(int, fget=processingState, fset=setProcessingState) def setProgress(self, progress): """ Set the node work progress state (number between 0 and 100). """ if progress is None or progress < 0: progress = -1 progress = max(min(progress, 100), -1) if self.__progress != progress: self.__progress = progress self.shapeItem.setProgress(progress) self.__updateTitleText() def progress(self): """ Return the node work progress state. """ return self.__progress progress_ = Property(float, fget=progress, fset=setProgress, doc="Node progress state.") def setProgressMessage(self, message): """ Set the node work progress message. .. note:: Not yet implemented """ pass def setErrorMessage(self, message): if self.__error != message: self.__error = message self.__updateMessages() def setWarningMessage(self, message): if self.__warning != message: self.__warning = message self.__updateMessages() def setInfoMessage(self, message): if self.__info != message: self.__info = message self.__updateMessages() def newInputAnchor(self): """ Create and return a new input :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") anchor = AnchorPoint() self.inputAnchorItem.addAnchor(anchor, position=1.0) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) return anchor def removeInputAnchor(self, anchor): """ Remove input anchor. """ self.inputAnchorItem.removeAnchor(anchor) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) def newOutputAnchor(self): """ Create and return a new output :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") anchor = AnchorPoint(self) self.outputAnchorItem.addAnchor(anchor, position=1.0) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) return anchor def removeOutputAnchor(self, anchor): """ Remove output anchor. """ self.outputAnchorItem.removeAnchor(anchor) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) def inputAnchors(self): """ Return a list of all input anchor points. """ return self.inputAnchorItem.anchorPoints() def outputAnchors(self): """ Return a list of all output anchor points. """ return self.outputAnchorItem.anchorPoints() def setAnchorRotation(self, angle): """ Set the anchor rotation. """ self.inputAnchorItem.setRotation(angle) self.outputAnchorItem.setRotation(angle) self.anchorGeometryChanged.emit() def anchorRotation(self): """ Return the anchor rotation. """ return self.inputAnchorItem.rotation() def boundingRect(self): # TODO: Important because of this any time the child # items change geometry the self.prepareGeometryChange() # needs to be called. return self.childrenBoundingRect() def shape(self): # Shape for mouse hit detection. # TODO: Should this return the union of all child items? return self.shapeItem.shape() def __updateTitleText(self): """ Update the title text item. """ title_safe = escape(self.title()) if self.progress() > 0: text = '<div align="center">%s<br/>%i%%</div>' % \ (title_safe, int(self.progress())) else: text = '<div align="center">%s</div>' % \ (title_safe) # The NodeItems boundingRect could change. self.prepareGeometryChange() self.captionTextItem.setHtml(text) self.captionTextItem.document().adjustSize() width = self.captionTextItem.textWidth() self.captionTextItem.setPos(-width / 2.0, 33) def __updateMessages(self): """ Update message items (position, visibility and tool tips). """ items = [self.errorItem, self.warningItem, self.infoItem] messages = [self.__error, self.__warning, self.__info] for message, item in zip(messages, items): item.setVisible(bool(message)) item.setToolTip(message or "") shown = [item for item in items if item.isVisible()] count = len(shown) if count: spacing = 3 rects = [item.boundingRect() for item in shown] width = sum(rect.width() for rect in rects) width += spacing * max(0, count - 1) height = max(rect.height() for rect in rects) origin = self.shapeItem.boundingRect().top() - spacing - height origin = QPointF(-width / 2, origin) for item, rect in zip(shown, rects): item.setPos(origin) origin = origin + QPointF(rect.width() + spacing, 0) def mousePressEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.mousePressEvent(self, event) else: event.ignore() def mouseDoubleClickEvent(self, event): if self.shapeItem.path().contains(event.pos()): QGraphicsObject.mouseDoubleClickEvent(self, event) QTimer.singleShot(0, self.activated.emit) else: event.ignore() def contextMenuEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.contextMenuEvent(self, event) else: event.ignore() def focusInEvent(self, event): self.shapeItem.setHasFocus(True) return QGraphicsObject.focusInEvent(self, event) def focusOutEvent(self, event): self.shapeItem.setHasFocus(False) return QGraphicsObject.focusOutEvent(self, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: self.shapeItem.setSelected(value.toBool()) elif change == QGraphicsItem.ItemPositionHasChanged: self.positionChanged.emit() return QGraphicsObject.itemChange(self, change, value)
class OWAxis(QGraphicsItem): Role = OWPalette.Axis def __init__(self, id, title='', title_above=False, title_location=AxisMiddle, line=None, arrows=AxisEnd, plot=None): QGraphicsItem.__init__(self) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setZValue(AxisZValue) self.id = id self.title = title self.title_location = title_location self.data_line = line self.plot = plot self.graph_line = None self.size = None self.scale = None self.tick_length = (10, 5, 0) self.arrows = arrows self.title_above = title_above self.line_item = QGraphicsLineItem(self) self.title_item = QGraphicsTextItem(self) self.end_arrow_item = None self.start_arrow_item = None self.show_title = False self.scale = None path = QPainterPath() path.setFillRule(Qt.WindingFill) path.moveTo(0, 3.09) path.lineTo(0, -3.09) path.lineTo(9.51, 0) path.closeSubpath() self.arrow_path = path self.label_items = [] self.label_bg_items = [] self.tick_items = [] self._ticks = [] self.zoom_transform = QTransform() self.labels = None self.auto_range = None self.auto_scale = True self.zoomable = False self.update_callback = None self.max_text_width = 50 self.text_margin = 5 self.always_horizontal_text = False def update_ticks(self): self._ticks = [] major, medium, minor = self.tick_length if self.labels is not None and not self.auto_scale: for i, text in enumerate(self.labels): self._ticks.append((i, text, medium, 1)) else: if self.scale and not self.auto_scale: min, max, step = self.scale elif self.auto_range: min, max = self.auto_range if min is not None and max is not None: step = (max - min) / 10 else: return else: return if max == min: return magnitude = int(3 * log10(abs(max - min)) + 1) if magnitude % 3 == 0: first_place = 1 elif magnitude % 3 == 1: first_place = 2 else: first_place = 5 magnitude = magnitude / 3 - 1 step = first_place * pow(10, magnitude) val = ceil(min / step) * step while val <= max: self._ticks.append((val, "%.4g" % val, medium, step)) val = val + step def update_graph(self): if self.update_callback: self.update_callback() def update(self, zoom_only=False): self.update_ticks() line_color = self.plot.color(OWPalette.Axis) text_color = self.plot.color(OWPalette.Text) if not self.graph_line or not self.scene(): return self.line_item.setLine(self.graph_line) self.line_item.setPen(line_color) if self.title: self.title_item.setHtml('<b>' + self.title + '</b>') self.title_item.setDefaultTextColor(text_color) if self.title_location == AxisMiddle: title_p = 0.5 elif self.title_location == AxisEnd: title_p = 0.95 else: title_p = 0.05 title_pos = self.graph_line.pointAt(title_p) v = self.graph_line.normalVector().unitVector() dense_text = False if hasattr(self, 'title_margin'): offset = self.title_margin elif self._ticks: if self.should_be_expanded(): offset = 55 dense_text = True else: offset = 35 else: offset = 10 if self.title_above: title_pos = title_pos + (v.p2() - v.p1()) * ( offset + QFontMetrics(self.title_item.font()).height()) else: title_pos = title_pos - (v.p2() - v.p1()) * offset ## TODO: Move it according to self.label_pos self.title_item.setVisible(self.show_title) self.title_item.setRotation(-self.graph_line.angle()) c = self.title_item.mapToParent( self.title_item.boundingRect().center()) tl = self.title_item.mapToParent( self.title_item.boundingRect().topLeft()) self.title_item.setPos(title_pos - c + tl) ## Arrows if not zoom_only: if self.start_arrow_item: self.scene().removeItem(self.start_arrow_item) self.start_arrow_item = None if self.end_arrow_item: self.scene().removeItem(self.end_arrow_item) self.end_arrow_item = None if self.arrows & AxisStart: if not zoom_only or not self.start_arrow_item: self.start_arrow_item = QGraphicsPathItem( self.arrow_path, self) self.start_arrow_item.setPos(self.graph_line.p1()) self.start_arrow_item.setRotation(-self.graph_line.angle() + 180) self.start_arrow_item.setBrush(line_color) self.start_arrow_item.setPen(line_color) if self.arrows & AxisEnd: if not zoom_only or not self.end_arrow_item: self.end_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.end_arrow_item.setPos(self.graph_line.p2()) self.end_arrow_item.setRotation(-self.graph_line.angle()) self.end_arrow_item.setBrush(line_color) self.end_arrow_item.setPen(line_color) ## Labels n = len(self._ticks) resize_plot_item_list(self.label_items, n, QGraphicsTextItem, self) resize_plot_item_list(self.label_bg_items, n, QGraphicsRectItem, self) resize_plot_item_list(self.tick_items, n, QGraphicsLineItem, self) test_rect = QRectF(self.graph_line.p1(), self.graph_line.p2()).normalized() test_rect.adjust(-1, -1, 1, 1) n_v = self.graph_line.normalVector().unitVector() if self.title_above: n_p = n_v.p2() - n_v.p1() else: n_p = n_v.p1() - n_v.p2() l_v = self.graph_line.unitVector() l_p = l_v.p2() - l_v.p1() for i in range(n): pos, text, size, step = self._ticks[i] hs = 0.5 * step tick_pos = self.map_to_graph(pos) if not test_rect.contains(tick_pos): self.tick_items[i].setVisible(False) self.label_items[i].setVisible(False) continue item = self.label_items[i] item.setVisible(True) if not zoom_only: if self.id in XAxes or getattr(self, 'is_horizontal', False): item.setHtml('<center>' + Qt.escape(text.strip()) + '</center>') else: item.setHtml(Qt.escape(text.strip())) item.setTextWidth(-1) text_angle = 0 if dense_text: w = min(item.boundingRect().width(), self.max_text_width) item.setTextWidth(w) if self.title_above: label_pos = tick_pos + n_p * ( w + self.text_margin ) + l_p * item.boundingRect().height() / 2 else: label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect( ).height() / 2 text_angle = -90 if self.title_above else 90 else: w = min( item.boundingRect().width(), QLineF(self.map_to_graph(pos - hs), self.map_to_graph(pos + hs)).length()) label_pos = tick_pos + n_p * self.text_margin - l_p * w / 2 item.setTextWidth(w) if not self.always_horizontal_text: if self.title_above: item.setRotation(-self.graph_line.angle() - text_angle) else: item.setRotation(self.graph_line.angle() - text_angle) item.setPos(label_pos) item.setDefaultTextColor(text_color) self.label_bg_items[i].setRect(item.boundingRect()) self.label_bg_items[i].setPen(QPen(Qt.NoPen)) self.label_bg_items[i].setBrush(self.plot.color(OWPalette.Canvas)) item = self.tick_items[i] item.setVisible(True) tick_line = QLineF(v) tick_line.translate(-tick_line.p1()) tick_line.setLength(size) if self.title_above: tick_line.setAngle(tick_line.angle() + 180) item.setLine(tick_line) item.setPen(line_color) item.setPos(self.map_to_graph(pos)) @staticmethod def make_title(label, unit=None): lab = '<i>' + label + '</i>' if unit: lab = lab + ' [' + unit + ']' return lab def set_line(self, line): self.graph_line = line self.update() def set_title(self, title): self.title = title self.update() def set_show_title(self, b): self.show_title = b self.update() def set_labels(self, labels): self.labels = labels self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_scale(self, min, max, step_size): self.scale = (min, max, step_size) self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_tick_length(self, minor, medium, major): self.tick_length = (minor, medium, major) self.update() def map_to_graph(self, x): min, max = self.plot.bounds_for_axis(self.id) if min == max: return QPointF() line_point = self.graph_line.pointAt((x - min) / (max - min)) end_point = line_point * self.zoom_transform return self.projection(end_point, self.graph_line) @staticmethod def projection(point, line): norm = line.normalVector() norm.translate(point - norm.p1()) p = QPointF() type = line.intersect(norm, p) return p def continuous_labels(self): min, max, step = self.scale magnitude = log10(abs(max - min)) def paint(self, painter, option, widget): pass def boundingRect(self): return QRectF() def ticks(self): if not self._ticks: self.update_ticks() return self._ticks def bounds(self): if self.labels: return -0.2, len(self.labels) - 0.8 elif self.scale: min, max, _step = self.scale return min, max elif self.auto_range: return self.auto_range else: return 0, 1 def should_be_expanded(self): self.update_ticks() return self.id in YAxes or self.always_horizontal_text or sum( len(t[1]) for t in self._ticks) * 12 > self.plot.width()
class OWAxis(QGraphicsItem): Role = OWPalette.Axis def __init__(self, id, title = '', title_above = False, title_location = AxisMiddle, line = None, arrows = 0, plot = None): QGraphicsItem.__init__(self) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setZValue(AxisZValue) self.id = id self.title = title self.title_location = title_location self.data_line = line self.plot = plot self.graph_line = None self.size = None self.scale = None self.tick_length = (10, 5, 0) self.arrows = arrows self.title_above = title_above self.line_item = QGraphicsLineItem(self) self.title_item = QGraphicsTextItem(self) self.end_arrow_item = None self.start_arrow_item = None self.show_title = False self.scale = None path = QPainterPath() path.setFillRule(Qt.WindingFill) path.moveTo(0, 3.09) path.lineTo(0, -3.09) path.lineTo(9.51, 0) path.closeSubpath() self.arrow_path = path self.label_items = [] self.label_bg_items = [] self.tick_items = [] self._ticks = [] self.zoom_transform = QTransform() self.labels = None self.auto_range = None self.auto_scale = True self.zoomable = False self.update_callback = None self.max_text_width = 50 self.text_margin = 5 self.always_horizontal_text = False @staticmethod def compute_scale(min, max): magnitude = int(3*log10(abs(max-min)) + 1) if magnitude % 3 == 0: first_place = 1 elif magnitude % 3 == 1: first_place = 2 else: first_place = 5 magnitude = magnitude // 3 - 1 step = first_place * pow(10, magnitude) first_val = ceil(min/step) * step return first_val, step def update_ticks(self): self._ticks = [] major, medium, minor = self.tick_length if self.labels is not None and not self.auto_scale: for i, text in enumerate(self.labels): self._ticks.append( ( i, text, medium, 1 ) ) else: if self.scale and not self.auto_scale: min, max, step = self.scale elif self.auto_range: min, max = self.auto_range if min is not None and max is not None: step = (max - min)/10 else: return else: return if max == min: return val, step = self.compute_scale(min, max) while val <= max: self._ticks.append( ( val, "%.4g" % val, medium, step ) ) val += step def update_graph(self): if self.update_callback: self.update_callback() def update(self, zoom_only = False): self.update_ticks() line_color = self.plot.color(OWPalette.Axis) text_color = self.plot.color(OWPalette.Text) if not self.graph_line or not self.scene(): return self.line_item.setLine(self.graph_line) self.line_item.setPen(line_color) if self.title: self.title_item.setHtml('<b>' + self.title + '</b>') self.title_item.setDefaultTextColor(text_color) if self.title_location == AxisMiddle: title_p = 0.5 elif self.title_location == AxisEnd: title_p = 0.95 else: title_p = 0.05 title_pos = self.graph_line.pointAt(title_p) v = self.graph_line.normalVector().unitVector() dense_text = False if hasattr(self, 'title_margin'): offset = self.title_margin elif self._ticks: if self.should_be_expanded(): offset = 55 dense_text = True else: offset = 35 else: offset = 10 if self.title_above: title_pos = title_pos + (v.p2() - v.p1())*(offset + QFontMetrics(self.title_item.font()).height()) else: title_pos = title_pos - (v.p2() - v.p1())*offset ## TODO: Move it according to self.label_pos self.title_item.setVisible(self.show_title) self.title_item.setRotation(-self.graph_line.angle()) c = self.title_item.mapToParent(self.title_item.boundingRect().center()) tl = self.title_item.mapToParent(self.title_item.boundingRect().topLeft()) self.title_item.setPos(title_pos - c + tl) ## Arrows if not zoom_only: if self.start_arrow_item: self.scene().removeItem(self.start_arrow_item) self.start_arrow_item = None if self.end_arrow_item: self.scene().removeItem(self.end_arrow_item) self.end_arrow_item = None if self.arrows & AxisStart: if not zoom_only or not self.start_arrow_item: self.start_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.start_arrow_item.setPos(self.graph_line.p1()) self.start_arrow_item.setRotation(-self.graph_line.angle() + 180) self.start_arrow_item.setBrush(line_color) self.start_arrow_item.setPen(line_color) if self.arrows & AxisEnd: if not zoom_only or not self.end_arrow_item: self.end_arrow_item = QGraphicsPathItem(self.arrow_path, self) self.end_arrow_item.setPos(self.graph_line.p2()) self.end_arrow_item.setRotation(-self.graph_line.angle()) self.end_arrow_item.setBrush(line_color) self.end_arrow_item.setPen(line_color) ## Labels n = len(self._ticks) resize_plot_item_list(self.label_items, n, QGraphicsTextItem, self) resize_plot_item_list(self.label_bg_items, n, QGraphicsRectItem, self) resize_plot_item_list(self.tick_items, n, QGraphicsLineItem, self) test_rect = QRectF(self.graph_line.p1(), self.graph_line.p2()).normalized() test_rect.adjust(-1, -1, 1, 1) n_v = self.graph_line.normalVector().unitVector() if self.title_above: n_p = n_v.p2() - n_v.p1() else: n_p = n_v.p1() - n_v.p2() l_v = self.graph_line.unitVector() l_p = l_v.p2() - l_v.p1() for i in range(n): pos, text, size, step = self._ticks[i] hs = 0.5 * step tick_pos = self.map_to_graph( pos ) if not test_rect.contains(tick_pos): self.tick_items[i].setVisible(False) self.label_items[i].setVisible(False) continue item = self.label_items[i] item.setVisible(True) if not zoom_only: if self.id in XAxes or getattr(self, 'is_horizontal', False): item.setHtml( '<center>' + Qt.escape(text.strip()) + '</center>') else: item.setHtml(Qt.escape(text.strip())) item.setTextWidth(-1) text_angle = 0 if dense_text: w = min(item.boundingRect().width(), self.max_text_width) item.setTextWidth(w) if self.title_above: label_pos = tick_pos + n_p * (w + self.text_margin) + l_p * item.boundingRect().height()/2 else: label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect().height()/2 text_angle = -90 if self.title_above else 90 else: w = min(item.boundingRect().width(), QLineF(self.map_to_graph(pos - hs), self.map_to_graph(pos + hs) ).length()) label_pos = tick_pos + n_p * self.text_margin - l_p * w/2 item.setTextWidth(w) if not self.always_horizontal_text: if self.title_above: item.setRotation(-self.graph_line.angle() - text_angle) else: item.setRotation(self.graph_line.angle() - text_angle) item.setPos(label_pos) item.setDefaultTextColor(text_color) self.label_bg_items[i].setRect(item.boundingRect()) self.label_bg_items[i].setPen(QPen(Qt.NoPen)) self.label_bg_items[i].setBrush(self.plot.color(OWPalette.Canvas)) item = self.tick_items[i] item.setVisible(True) tick_line = QLineF(v) tick_line.translate(-tick_line.p1()) tick_line.setLength(size) if self.title_above: tick_line.setAngle(tick_line.angle() + 180) item.setLine( tick_line ) item.setPen(line_color) item.setPos(self.map_to_graph(pos)) @staticmethod def make_title(label, unit = None): lab = '<i>' + label + '</i>' if unit: lab = lab + ' [' + unit + ']' return lab def set_line(self, line): self.graph_line = line self.update() def set_title(self, title): self.title = title self.update() def set_show_title(self, b): self.show_title = b self.update() def set_labels(self, labels): self.labels = labels self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_scale(self, min, max, step_size): self.scale = (min, max, step_size) self.graph_line = None self.auto_scale = False self.update_ticks() self.update_graph() def set_tick_length(self, minor, medium, major): self.tick_length = (minor, medium, major) self.update() def map_to_graph(self, x): min, max = self.plot.bounds_for_axis(self.id) if min == max: return QPointF() line_point = self.graph_line.pointAt( (x-min)/(max-min) ) end_point = line_point * self.zoom_transform return self.projection(end_point, self.graph_line) @staticmethod def projection(point, line): norm = line.normalVector() norm.translate(point - norm.p1()) p = QPointF() type = line.intersect(norm, p) return p def continuous_labels(self): min, max, step = self.scale magnitude = log10(abs(max-min)) def paint(self, painter, option, widget): pass def boundingRect(self): return QRectF() def ticks(self): if not self._ticks: self.update_ticks() return self._ticks def bounds(self): if self.labels: return -0.2, len(self.labels) -0.8 elif self.scale: min, max, _step = self.scale return min, max elif self.auto_range: return self.auto_range else: return 0, 1 def should_be_expanded(self): self.update_ticks() return self.id in YAxes or self.always_horizontal_text or sum(len(t[1]) for t in self._ticks) * 12 > self.plot.width()
class NodeItem(QGraphicsObject): """ An widget node item in the canvas. """ #: Signal emitted when the scene position of the node has changed. positionChanged = Signal() #: Signal emitted when the geometry of the channel anchors changes. anchorGeometryChanged = Signal() #: Signal emitted when the item has been activated (by a mouse double #: click or a keyboard) activated = Signal() #: The item is under the mouse. hovered = Signal() #: Span of the anchor in degrees ANCHOR_SPAN_ANGLE = 90 #: Z value of the item Z_VALUE = 100 def __init__(self, widget_description=None, parent=None, **kwargs): QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QGraphicsItem.ItemHasNoContents, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) # central body shape item self.shapeItem = None # in/output anchor items self.inputAnchorItem = None self.outputAnchorItem = None # title text item self.captionTextItem = None # error, warning, info items self.errorItem = None self.warningItem = None self.infoItem = None self.__title = "" self.__processingState = 0 self.__progress = -1 self.__error = None self.__warning = None self.__info = None self.__anchorLayout = None self.__animationEnabled = False self.setZValue(self.Z_VALUE) self.setupGraphics() self.setWidgetDescription(widget_description) @classmethod def from_node(cls, node): """ Create an :class:`NodeItem` instance and initialize it from a :class:`SchemeNode` instance. """ self = cls() self.setWidgetDescription(node.description) # self.setCategoryDescription(node.category) return self @classmethod def from_node_meta(cls, meta_description): """ Create an `NodeItem` instance from a node meta description. """ self = cls() self.setWidgetDescription(meta_description) return self def setupGraphics(self): """ Set up the graphics. """ shape_rect = QRectF(-24, -24, 48, 48) self.shapeItem = NodeBodyItem(self) self.shapeItem.setShapeRect(shape_rect) self.shapeItem.setAnimationEnabled(self.__animationEnabled) # Rect for widget's 'ears'. anchor_rect = QRectF(-31, -31, 62, 62) self.inputAnchorItem = SinkAnchorItem(self) input_path = QPainterPath() start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 input_path.arcMoveTo(anchor_rect, start_angle) input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) self.inputAnchorItem.setAnchorPath(input_path) self.outputAnchorItem = SourceAnchorItem(self) output_path = QPainterPath() start_angle = self.ANCHOR_SPAN_ANGLE / 2 output_path.arcMoveTo(anchor_rect, start_angle) output_path.arcTo(anchor_rect, start_angle, -self.ANCHOR_SPAN_ANGLE) self.outputAnchorItem.setAnchorPath(output_path) self.inputAnchorItem.hide() self.outputAnchorItem.hide() # Title caption item self.captionTextItem = QGraphicsTextItem(self) self.captionTextItem.setPlainText("") self.captionTextItem.setPos(0, 33) def iconItem(standard_pixmap): item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), iconSize=QSize(16, 16)) item.hide() return item self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) # TODO: Remove the set[Widget|Category]Description. The user should # handle setting of icons, title, ... def setWidgetDescription(self, desc): """ Set widget description. """ self.widget_description = desc if desc is None: return icon = icon_loader.from_description(desc).get(desc.icon) if icon: self.setIcon(icon) if not self.title(): self.setTitle(desc.name) if desc.inputs: self.inputAnchorItem.show() if desc.outputs: self.outputAnchorItem.show() tooltip = NodeItem_toolTipHelper(self) self.setToolTip(tooltip) def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color) def setIcon(self, icon): """ Set the node item's icon (:class:`QIcon`). """ if isinstance(icon, QIcon): self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, iconSize=QSize(36, 36)) self.icon_item.setPos(-18, -18) else: raise TypeError def setColor(self, color, selectedColor=None): """ Set the widget color. """ if selectedColor is None: selectedColor = saturated(color, 150) palette = create_palette(color, selectedColor) self.shapeItem.setPalette(palette) def setPalette(self, palette): # TODO: The palette should override the `setColor` raise NotImplementedError def setTitle(self, title): """ Set the node title. The title text is displayed at the bottom of the node. """ self.__title = title self.__updateTitleText() def title(self): """ Return the node title. """ return self.__title title_ = Property(unicode, fget=title, fset=setTitle, doc="Node title text.") def setFont(self, font): """ Set the title text font (:class:`QFont`). """ if font != self.font(): self.prepareGeometryChange() self.captionTextItem.setFont(font) self.__updateTitleText() def font(self): """ Return the title text font. """ return self.captionTextItem.font() def setAnimationEnabled(self, enabled): """ Set the node animation enabled state. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled self.shapeItem.setAnimationEnabled(enabled) def animationEnabled(self): """ Are node animations enabled. """ return self.__animationEnabled def setProcessingState(self, state): """ Set the node processing state i.e. the node is processing (is busy) or is idle. """ if self.__processingState != state: self.__processingState = state self.shapeItem.setProcessingState(state) if not state: # Clear the progress meter. self.setProgress(-1) if self.__animationEnabled: self.shapeItem.ping() def processingState(self): """ The node processing state. """ return self.__processingState processingState_ = Property(int, fget=processingState, fset=setProcessingState) def setProgress(self, progress): """ Set the node work progress state (number between 0 and 100). """ if progress is None or progress < 0: progress = -1 progress = max(min(progress, 100), -1) if self.__progress != progress: self.__progress = progress self.shapeItem.setProgress(progress) self.__updateTitleText() def progress(self): """ Return the node work progress state. """ return self.__progress progress_ = Property(float, fget=progress, fset=setProgress, doc="Node progress state.") def setProgressMessage(self, message): """ Set the node work progress message. .. note:: Not yet implemented """ pass def setErrorMessage(self, message): if self.__error != message: self.__error = message self.__updateMessages() def setWarningMessage(self, message): if self.__warning != message: self.__warning = message self.__updateMessages() def setInfoMessage(self, message): if self.__info != message: self.__info = message self.__updateMessages() def newInputAnchor(self): """ Create and return a new input :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") anchor = AnchorPoint() self.inputAnchorItem.addAnchor(anchor, position=1.0) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) return anchor def removeInputAnchor(self, anchor): """ Remove input anchor. """ self.inputAnchorItem.removeAnchor(anchor) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) def newOutputAnchor(self): """ Create and return a new output :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") anchor = AnchorPoint(self) self.outputAnchorItem.addAnchor(anchor, position=1.0) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) return anchor def removeOutputAnchor(self, anchor): """ Remove output anchor. """ self.outputAnchorItem.removeAnchor(anchor) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) def inputAnchors(self): """ Return a list of all input anchor points. """ return self.inputAnchorItem.anchorPoints() def outputAnchors(self): """ Return a list of all output anchor points. """ return self.outputAnchorItem.anchorPoints() def setAnchorRotation(self, angle): """ Set the anchor rotation. """ self.inputAnchorItem.setRotation(angle) self.outputAnchorItem.setRotation(angle) self.anchorGeometryChanged.emit() def anchorRotation(self): """ Return the anchor rotation. """ return self.inputAnchorItem.rotation() def boundingRect(self): # TODO: Important because of this any time the child # items change geometry the self.prepareGeometryChange() # needs to be called. return self.childrenBoundingRect() def shape(self): # Shape for mouse hit detection. # TODO: Should this return the union of all child items? return self.shapeItem.shape() def __updateTitleText(self): """ Update the title text item. """ title_safe = escape(self.title()) if self.progress() > 0: text = '<div align="center">%s<br/>%i%%</div>' % \ (title_safe, int(self.progress())) else: text = '<div align="center">%s</div>' % \ (title_safe) # The NodeItems boundingRect could change. self.prepareGeometryChange() self.captionTextItem.setHtml(text) self.captionTextItem.document().adjustSize() width = self.captionTextItem.textWidth() self.captionTextItem.setPos(-width / 2.0, 33) def __updateMessages(self): """ Update message items (position, visibility and tool tips). """ items = [self.errorItem, self.warningItem, self.infoItem] messages = [self.__error, self.__warning, self.__info] for message, item in zip(messages, items): item.setVisible(bool(message)) item.setToolTip(message or "") shown = [item for item in items if item.isVisible()] count = len(shown) if count: spacing = 3 rects = [item.boundingRect() for item in shown] width = sum(rect.width() for rect in rects) width += spacing * max(0, count - 1) height = max(rect.height() for rect in rects) origin = self.shapeItem.boundingRect().top() - spacing - height origin = QPointF(-width / 2, origin) for item, rect in zip(shown, rects): item.setPos(origin) origin = origin + QPointF(rect.width() + spacing, 0) def mousePressEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.mousePressEvent(self, event) else: event.ignore() def mouseDoubleClickEvent(self, event): if self.shapeItem.path().contains(event.pos()): QGraphicsObject.mouseDoubleClickEvent(self, event) QTimer.singleShot(0, self.activated.emit) else: event.ignore() def contextMenuEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.contextMenuEvent(self, event) else: event.ignore() def focusInEvent(self, event): self.shapeItem.setHasFocus(True) return QGraphicsObject.focusInEvent(self, event) def focusOutEvent(self, event): self.shapeItem.setHasFocus(False) return QGraphicsObject.focusOutEvent(self, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: self.shapeItem.setSelected(value.toBool()) elif change == QGraphicsItem.ItemPositionHasChanged: self.positionChanged.emit() return QGraphicsObject.itemChange(self, change, value)