def update_items(self): self.item = QGraphicsRectItem(0, 0, self.width, self.row_h * self.coeff_h) seq_width = 0 nopen = QPen(Qt.NoPen) self.item.setPen(nopen) font = QFont(self.ftype, self.fsize) if self.fstyle == "italic": font.setStyle(QFont.StyleItalic) elif self.fstyle == "oblique": font.setStyle(QFont.StyleOblique) rect_cls = QGraphicsRectItem for i, val in enumerate(self.liste): width = self.col_w height = self.row_h * len(str(val)) + 1 rectitem = rect_cls(0, 0, width, height, parent=self.item) rectitem.setX(seq_width) # to give correct X to children item rectitem.setBrush(QBrush(QColor(self.bgcolor))) rectitem.setPen(nopen) # write letter if enough space in height if height >= self.fsize: text = QGraphicsSimpleTextItem(str(val), parent=rectitem) text.setFont(font) text.setBrush(QBrush(QColor(self.fgcolor))) # Center text according to rectitem size txtw = text.boundingRect().width() / 3.0 txth = text.boundingRect().height() text.setRotation(self.rot) text.setX(txth * 1.5) #text.setY(0) seq_width += width self.width = seq_width
def _adjustCheciItem(self, textItem: QtWidgets.QGraphicsSimpleTextItem, startPoint: QtCore.QPointF, endPoint: QtCore.QPointF): """ 沿着斜线向endPoint方向调整车次标签的位置。必须保证垂直方向不越界。 """ minY = min((startPoint.y(), endPoint.y())) maxY = max((startPoint.y(), endPoint.y())) w, h = textItem.boundingRect().width(), textItem.boundingRect().height( ) if endPoint.x() == startPoint.x(): # 斜率不存在的特殊情况 dx = 0 dy = h elif endPoint.y() == startPoint.y(): # k=0的特殊情况 dy = h dx = w else: k = (endPoint.y() - startPoint.y()) / (endPoint.x() - startPoint.x()) dy = h dx = dy / k if endPoint.y() < startPoint.y(): dy = -dy dx = -dx x, y = textItem.x(), textItem.y() while minY <= y <= maxY: for item in textItem.collidingItems(): if isinstance(item, QtWidgets.QGraphicsSimpleTextItem): x += dx y += dy textItem.setPos(x, y) break else: break
class FloatingTextWidget(QGraphicsWidget): def __init__(self, parent=None, anchor="center"): QGraphicsWidget.__init__(self, parent) assert anchor in {"center", "corner"} self.anchor = anchor self._label = QGraphicsSimpleTextItem(self) self._label.setBrush(QColor(255, 255, 255)) # Add drop shadow self._dropShadowEffect = QGraphicsDropShadowEffect() self.setGraphicsEffect(self._dropShadowEffect) self._dropShadowEffect.setOffset(0.0, 10.0) self._dropShadowEffect.setBlurRadius(8.0) self._dropShadowEffect.setColor(QColor(0, 0, 0, 50)) self._spacingConstant = 5.0 def updateLayout(self): width = self._label.boundingRect().width() height = self._label.boundingRect().height() width = self._spacingConstant + width + self._spacingConstant height = self._spacingConstant + height + self._spacingConstant self._label.setPos(self._spacingConstant, self._spacingConstant) self.resize(width, height) self.update() def paint(self, painter, option, widget): shape = QPainterPath() shape.addRoundedRect(self.rect(), 1, 1) painter.setBrush(QBrush(QColor(0, 0, 0))) painter.drawPath(shape) # painter.setPen(self._pen) # painter.drawPath(self._path) def onUpdated(self, center_position, text): self._label.setText(text) self.updateLayout() rect = self.rect() x_pos = center_position.x() y_pos = center_position.y() if self.anchor == "center": x_pos -= rect.width() / 2 y_pos -= rect.height() / 2 else: y_pos -= rect.height() self.setPos(x_pos, y_pos)
class WotNode(BaseNode): def __init__(self, nx_node, pos): """ Create node in the graph scene :param tuple nx_node: Node info :param x_y: Position of the node """ super().__init__(nx_node, pos) # color around ellipse outline_color = QColor('grey') outline_style = Qt.SolidLine outline_width = 1 if self.status_wallet: outline_color = QColor('black') outline_width = 2 if not self.status_member: outline_color = QColor('red') outline_style = Qt.SolidLine self.setPen(QPen(outline_color, outline_width, outline_style)) # text inside ellipse self.text_item = QGraphicsSimpleTextItem(self) self.text_item.setText(self.text) text_color = QColor('grey') if self.status_wallet == NodeStatus.HIGHLIGHTED: text_color = QColor('black') self.text_item.setBrush(QBrush(text_color)) # center ellipse around text self.setRect(0, 0, self.text_item.boundingRect().width() * 2, self.text_item.boundingRect().height() * 2) # set anchor to the center self.setTransform(QTransform().translate( -self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) # center text in ellipse self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) # create gradient inside the ellipse gradient = QRadialGradient( QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width()) gradient.setColorAt(0, QColor('white')) gradient.setColorAt(1, QColor('darkgrey')) self.setBrush(QBrush(gradient)) # cursor change on hover self.setAcceptHoverEvents(True) self.setZValue(1)
class WotNode(BaseNode): def __init__(self, nx_node, pos): """ Create node in the graph scene :param tuple nx_node: Node info :param x_y: Position of the node """ super().__init__(nx_node, pos) # color around ellipse outline_color = QColor('grey') outline_style = Qt.SolidLine outline_width = 1 if self.status_wallet: outline_color = QColor('black') outline_width = 2 if not self.status_member: outline_color = QColor('red') outline_style = Qt.SolidLine self.setPen(QPen(outline_color, outline_width, outline_style)) # text inside ellipse self.text_item = QGraphicsSimpleTextItem(self) self.text_item.setText(self.text) text_color = QColor('grey') if self.status_wallet == NodeStatus.HIGHLIGHTED: text_color = QColor('black') self.text_item.setBrush(QBrush(text_color)) # center ellipse around text self.setRect( 0, 0, self.text_item.boundingRect().width() * 2, self.text_item.boundingRect().height() * 2 ) # set anchor to the center self.setTransform( QTransform().translate(-self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) # center text in ellipse self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) # create gradient inside the ellipse gradient = QRadialGradient(QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width()) gradient.setColorAt(0, QColor('white')) gradient.setColorAt(1, QColor('darkgrey')) self.setBrush(QBrush(gradient)) # cursor change on hover self.setAcceptHoverEvents(True) self.setZValue(1)
def __init__(self, x, y, width=STATION_WIDTH, height=STATION_HEIGHT, name=""): # 此处的初始化 xy 值是以 item coordinate 标记矩形的起始点(左上角) super().__init__(-width / 2, -height / 2, width, height) # 此处的 xy 值是以 scene coordinate 标记矩形,结合上面的初始化,标记点是矩形的几何中心 self.setPos(x, y) # 背景黑色 self.setBrush(black_brush) # icon(相对于父 item 的 xy 值) QGraphicsPixmapItem( QPixmap(RM_path("./source/station.png")).scaled( ICON_WIDTH, ICON_HEIGHT), self).setPos(-width / 2 + 8, -ICON_HEIGHT / 2) # name(相对于父 item 的 xy 值) name = QGraphicsSimpleTextItem(name, self) name.setBrush(white_brush) name.setFont(title_font) name.setPos(-width / 2 + 8 + ICON_WIDTH, -name.boundingRect().height() / 2)
class DiagramItem(QGraphicsPolygonItem): def __init__(self, framework_layer: LayerInterface, context_menu: QMenu, parent: QGraphicsItem = None): super(DiagramItem, self).__init__(parent) self.arrows = list() self.framework_layer = framework_layer self.context_menu = context_menu self.polygon = self.framework_layer.layer_image() self.create_text_item() self.setPolygon(self.polygon) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) def create_text_item(self): self.text_item = QGraphicsSimpleTextItem( self.framework_layer.layer_name(), self) rect = self.text_item.boundingRect() rect.moveCenter(self.boundingRect().center()) self.text_item.setPos(rect.topLeft()) def get_arrows(self) -> List['Arrow']: return self.arrows def get_framework_layer(self) -> LayerInterface: return self.framework_layer def add_arrow(self, arrow): self.arrows.append(arrow) def remove_arrow(self, arrow): try: self.arrows.remove(arrow) except ValueError: pass def remove_arrows(self): for arrow in self.arrows[:]: arrow.get_start_item().remove_arrow(arrow) arrow.get_end_item().remove_arrow(arrow) self.scene().removeItem(arrow) def mouseDoubleClickEvent(self, event): self.framework_layer.layer_config_dialog() def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent): self.scene().clearSelection() self.setSelected(True) self.context_menu.exec_(event.screenPos()) def itemChange(self, change: int, value: int) -> int: if change == QGraphicsItem.ItemPositionChange: for arrow in self.arrows: arrow.updatePosition() return value
def _adjustTimeItem(self, textItem: QtWidgets.QGraphicsSimpleTextItem, scale: int): """ 向远离运行线的方向调整冲突的始发终到时刻标签的位置。scale为正负1,表示移动方向。 """ w, h = textItem.boundingRect().width(), textItem.boundingRect().height( ) y = textItem.y() dy = h * scale while 0 <= y <= self.sizes["height"] - h: for item in textItem.collidingItems(): if isinstance(item, QtWidgets.QGraphicsSimpleTextItem): y += dy textItem.setY(y) break else: break
class BasicNode(QGraphicsItemGroup): def __init__(self, model, manager, text_color=Qt.black): bg_color = model.get_bg_color() super(BasicNode, self).__init__() self.model = model text = model.get_display_text() self.manager = manager self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) self.text_graph = QGraphicsSimpleTextItem(text) self.text_graph.setBrush(text_color) bound = self.text_graph.boundingRect() r = QPointF(bound.width() / 2, bound.height() / 2) text_center = self.text_graph.pos() + r self.text_graph.setPos(-text_center) self.addToGroup(self.text_graph) self.box_graph = BoxOutline(bg_color) empty_space = QPointF(UNIT, UNIT) newr = (empty_space + r) self.box_graph.rect = QRectF(-newr, newr) self.addToGroup(self.box_graph) self.text_graph.setZValue(1.0) self.box_graph.setZValue(0.0) self.children = [] def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: self.manager.selection_changed(self, value) return value else: return super(BasicNode, self).itemChange(change, value) def get_width(self): return self.boundingRect().width() def set_left_pos(self, pos): pos += QPoint(self.get_width() / 2.0, 0) self.setPos(pos) def left_pos(self): return self.pos() - QPointF(self.get_width() / 2.0, 0) def center_pos(self): return self.pos() def right_pos(self): return self.pos() + QPointF(self.get_width() / 2.0, 0) """
def drawHRefs(self): if self.xvmax < self.xvmin or self.awidth <= 0: return minsep = 30 factor = 1 unitincrement = self.awidth/float(self.xvmax-self.xvmin) xmaxint = self.xvmax vx = int(self.xvmin) pstart = self.value2point(vx, self.yvmin) px = pstart.x() pystart = pstart.y() pend = self.value2point(xmaxint, self.yvmin) pxend = pend.x() pyend = pend.y()-2 try: minsep = 10 * max([len(h) for h in self.hheaders]) except Exception: pass while (unitincrement*factor < minsep): provfactor = 2*factor if(unitincrement*provfactor > minsep): factor = provfactor break provfactor = 5*factor if(unitincrement*provfactor > minsep): factor = provfactor break factor = 10*factor # px+=unitincrement*factor # vx +=factor while(px <= pxend): colour = QtGui.QColor(0, 0, 0, 255) PlotLine(px+0.5, pystart+2, px+0.5, pyend, 1.5, colour, self) try: header = self.hheaders[vx] except IndexError: header = vx nlabel = QGraphicsSimpleTextItem( "{}".format(header), self) font = nlabel.font() font.setPixelSize(20) nlabel.setFont(font) nlabelrect = nlabel.boundingRect() nlabel.setPos(px + 0.5 - nlabelrect.width()/2, pystart+3) px += unitincrement*factor vx += factor
def addRectText(x, w, parent, text="", level=0, tooltip=""): deltaH = LEVEL_HEIGHT if level else 0 r = OutlineRect(0, 0, w, parent.rect().height()-deltaH, parent, title=text) r.setPos(x, deltaH) txt = QGraphicsSimpleTextItem(text, r) f = txt.font() f.setPointSize(8) fm = QFontMetricsF(f) elidedText = fm.elidedText(text, Qt.ElideMiddle, w) txt.setFont(f) txt.setText(elidedText) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) txt.setY(0) return r
def drawVRefs(self): if self.yvmax < self.yvmin or self.aheight <= 0: return minsep = 30 factor = 1 try: unitincrement = self.aheight/float(self.yvmax-self.yvmin) except ZeroDivisionError: msg = "Division by zero in drawVRefs. Limits are {}-{}" print(msg.format(self.yvmin, self.yvmax)) while (unitincrement*factor < minsep): provfactor = 2*factor if(unitincrement*provfactor > minsep): factor = provfactor break provfactor = 5*factor if(unitincrement*provfactor > minsep): factor = provfactor break factor = 10*factor if (self.yvmin <= 0): vy = int(self.yvmin/factor)*factor else: vy = (int(self.yvmin/factor)+1)*factor pstart = self.value2point(self.xvmin, vy) pxstart = pstart.x() py = pstart.y() pend = self.value2point(self.xvmax, self.yvmax) pxend = pend.x() pyend = pend.y() while(py > pyend): colour = QtGui.QColor(0, 0, 0, 200) if vy == 0: PlotLine(pxstart-2, py, pxend, py, 1.5, QtCore.Qt.black, self) else: PlotLine(pxstart-2, py, pxend, py, 0.5, colour, self) nlabel = QGraphicsSimpleTextItem("{}".format(vy), self) font = nlabel.font() font.setPixelSize(20) nlabel.setFont(font) nlabelrect = nlabel.boundingRect() nlabel.setPos(pxstart - nlabelrect.width() - 5, py-nlabelrect.height()/2) py -= unitincrement*factor vy += factor
class PackageItem(UMLItem): """ Class implementing a package item. """ ItemType = "package" def __init__(self, model=None, x=0, y=0, rounded=False, noModules=False, parent=None, scene=None): """ Constructor @param model package model containing the package data (PackageModel) @param x x-coordinate (integer) @param y y-coordinate (integer) @param rounded flag indicating a rounded corner (boolean) @keyparam noModules flag indicating, that no module names should be shown (boolean) @keyparam parent reference to the parent object (QGraphicsItem) @keyparam scene reference to the scene object (QGraphicsScene) """ UMLItem.__init__(self, model, x, y, rounded, parent) self.noModules = noModules scene.addItem(self) if self.model: self.__createTexts() self.__calculateSize() def __createTexts(self): """ Private method to create the text items of the class item. """ if self.model is None: return boldFont = QFont(self.font) boldFont.setBold(True) modules = self.model.getModules() x = self.margin + self.rect().x() y = self.margin + self.rect().y() self.header = QGraphicsSimpleTextItem(self) self.header.setFont(boldFont) self.header.setText(self.model.getName()) self.header.setPos(x, y) y += self.header.boundingRect().height() + self.margin if not self.noModules: if modules: txt = "\n".join(modules) else: txt = " " self.modules = QGraphicsSimpleTextItem(self) self.modules.setFont(self.font) self.modules.setText(txt) self.modules.setPos(x, y) else: self.modules = None def __calculateSize(self): """ Private method to calculate the size of the package widget. """ if self.model is None: return width = self.header.boundingRect().width() height = self.header.boundingRect().height() if self.modules: width = max(width, self.modules.boundingRect().width()) height = height + self.modules.boundingRect().height() latchW = width / 3.0 latchH = min(15.0, latchW) self.setSize(width + 2 * self.margin, height + latchH + 2 * self.margin) x = self.margin + self.rect().x() y = self.margin + self.rect().y() + latchH self.header.setPos(x, y) y += self.header.boundingRect().height() + self.margin if self.modules: self.modules.setPos(x, y) def setModel(self, model): """ Public method to set the package model. @param model package model containing the package data (PackageModel) """ self.scene().removeItem(self.header) self.header = None if self.modules: self.scene().removeItem(self.modules) self.modules = None self.model = model self.__createTexts() self.__calculateSize() def paint(self, painter, option, widget=None): """ Public method to paint the item in local coordinates. @param painter reference to the painter object (QPainter) @param option style options (QStyleOptionGraphicsItem) @param widget optional reference to the widget painted on (QWidget) """ pen = self.pen() if (option.state & QStyle.State_Selected) == \ QStyle.State(QStyle.State_Selected): pen.setWidth(2) else: pen.setWidth(1) offsetX = self.rect().x() offsetY = self.rect().y() w = self.rect().width() latchW = w / 3.0 latchH = min(15.0, latchW) h = self.rect().height() - latchH + 1 painter.setPen(pen) painter.setBrush(self.brush()) painter.setFont(self.font) painter.drawRect(offsetX, offsetY, latchW, latchH) painter.drawRect(offsetX, offsetY + latchH, w, h) y = self.margin + self.header.boundingRect().height() + latchH painter.drawLine(offsetX, offsetY + y, offsetX + w - 1, offsetY + y) self.adjustAssociations() def buildItemDataString(self): """ Public method to build a string to persist the specific item data. This string must start with ", " and should be built like "attribute=value" with pairs separated by ", ". value must not contain ", " or newlines. @return persistence data (string) """ entries = [ "no_modules={0}".format(self.noModules), "name={0}".format(self.model.getName()), ] modules = self.model.getModules() if modules: entries.append("modules={0}".format("||".join(modules))) return ", " + ", ".join(entries) def parseItemDataString(self, version, data): """ Public method to parse the given persistence data. @param version version of the data (string) @param data persisted data to be parsed (string) @return flag indicating success (boolean) """ parts = data.split(", ") if len(parts) < 2: return False name = "" modules = [] for part in parts: key, value = part.split("=", 1) if key == "no_modules": self.external = Utilities.toBool(value.strip()) elif key == "name": name = value.strip() elif key == "modules": modules = value.strip().split("||") else: return False self.model = PackageModel(name, modules) self.__createTexts() self.__calculateSize() return True
def create_info_display(self, x, y, attributes): """ Creates view elements for the info display :param x: x position of the node :param y: y position of the node :param attributes: attributes that will be displayed in the view :return: """ start_height = y + (self.NODE_HEIGHT / 2) # unfold dictionary values at the bottom of the list sorted_attributes = [] for k, v in sorted(attributes.items(), key=lambda tup: isinstance(tup[1], dict)): if isinstance(v, dict): sorted_attributes.append((k, v)) sorted_attributes.extend(v.items()) else: sorted_attributes.append((k, v)) # create property rows for i, (k, v) in enumerate(sorted_attributes): value_text = None value_height = 0 if isinstance(v, dict): # display dictionary key as title text = "{}".format(k) if len(text) > 20: text = text[:20] + "..." key_text = QGraphicsSimpleTextItem(text) f = key_text.font() f.setBold(True) key_text.setFont(f) text_width = key_text.boundingRect().width() else: key_text = QGraphicsSimpleTextItem("{}:".format(k) if k else " ") text = str(v) if len(text) > 20: text = text[:20] + "..." value_text = QGraphicsSimpleTextItem(text) value_height = value_text.boundingRect().height() text_width = key_text.boundingRect().width() + value_text.boundingRect().width() # create box around property attribute_container = QGraphicsRectItem(x, start_height, text_width + 10, max(key_text.boundingRect().height(), value_height) + 10) attribute_container.setBrush(QBrush(Qt.white)) self.total_height += attribute_container.rect().height() key_text.setParentItem(attribute_container) if value_text: value_text.setParentItem(attribute_container) self.max_width = max(self.max_width, attribute_container.rect().width()) attribute_container.setParentItem(self) self.info_display.append(attribute_container) start_height += max(key_text.boundingRect().height(), value_height) + 10 # calculate correct coordinates for positioning of the attribute boxes if self.max_width > self.NODE_MIN_WIDTH - 10: x -= (self.max_width + 10) / 2 y -= self.total_height / 2 self.max_width += 10 else: x -= self.NODE_MIN_WIDTH / 2 y -= self.total_height / 2 self.max_width = self.NODE_MIN_WIDTH h = 0 # position all the elements previously created for attribute_container in self.info_display: rect: QRectF = attribute_container.rect() rect.setX(x) rect_height = rect.height() rect.setY(y + self.NODE_HEIGHT + h) rect.setHeight(rect_height) key_child = attribute_container.childItems()[0] if len(attribute_container.childItems()) == 2: key_child.setX(x + 5) value_child = attribute_container.childItems()[1] value_child.setX(x + self.max_width - value_child.boundingRect().width() - 5) value_child.setY(y + self.NODE_HEIGHT + h + 5) else: key_child.setX(x - key_child.boundingRect().width() / 2 + self.max_width / 2) key_child.setY(y + self.NODE_HEIGHT + h + 5) h += rect.height() rect.setWidth(self.max_width) attribute_container.setRect(rect)
class StickWidget(QGraphicsObject): font: QFont = QFont("monospace", 32) delete_clicked = pyqtSignal(Stick) link_initiated = pyqtSignal('PyQt_PyObject') # Actually StickWidget link_accepted = pyqtSignal('PyQt_PyObject') hovered = pyqtSignal(['PyQt_PyObject', 'PyQt_PyObject']) stick_changed = pyqtSignal('PyQt_PyObject') sibling_changed = pyqtSignal(bool) right_clicked = pyqtSignal('PyQt_PyObject') handle_idle_brush = QBrush(QColor(0, 125, 125, 50)) handle_hover_brush = QBrush(QColor(125, 125, 0, 50)) handle_press_brush = QBrush(QColor(200, 200, 0, 0)) handle_idle_pen = QPen(QColor(0, 0, 0, 255)) handle_press_pen = QPen(QColor(200, 200, 0, 255)) handle_size = 20 normal_color = QColor(0, 200, 120) negative_color = QColor(200, 0, 0) positive_color = QColor(0, 200, 0) mismatched = pyqtSignal('PyQt_PyObject') misplaced = pyqtSignal('PyQt_PyObject') measurement_corrected = pyqtSignal('PyQt_PyObject') clearly_visible = pyqtSignal('PyQt_PyObject') zero_clicked = pyqtSignal('PyQt_PyObject') def __init__(self, stick: Stick, camera: Camera, parent: Optional[QGraphicsItem] = None): QGraphicsObject.__init__(self, parent) self.camera = camera self.stick = stick self.line = QLineF() self.gline = QGraphicsLineItem(self.line) self.stick_label_text = QGraphicsSimpleTextItem("0", self) self.stick_label_text.setFont(StickWidget.font) self.stick_label_text.setPos(self.line.p1() - QPoint(0, 24)) self.stick_label_text.setBrush(QBrush(QColor(0, 255, 0))) self.stick_label_text.hide() self.setZValue(10) self.mode = StickMode.Display self.btn_delete = Button("delete", "x", parent=self) self.btn_delete.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) self.btn_delete.set_base_color([ButtonColor.RED]) self.btn_delete.setVisible(False) btn_size = max(int(np.linalg.norm(self.stick.top - self.stick.bottom) / 5.0), 15) self.btn_delete.set_height(12) self.btn_delete.clicked.connect(self.handle_btn_delete_clicked) self.btn_delete.setPos(self.line.p1() - QPointF(0.5 * self.btn_delete.boundingRect().width(), 1.1 * self.btn_delete.boundingRect().height())) self.btn_delete.set_opacity(0.7) self.top_handle = QGraphicsEllipseItem(0, 0, self.handle_size, self.handle_size, self) self.mid_handle = QGraphicsEllipseItem(0, 0, self.handle_size, self.handle_size, self) self.bottom_handle = QGraphicsEllipseItem(0, 0, self.handle_size, self.handle_size, self) self.top_handle.setAcceptedMouseButtons(Qt.NoButton) self.mid_handle.setAcceptedMouseButtons(Qt.NoButton) self.bottom_handle.setAcceptedMouseButtons(Qt.NoButton) self.top_handle.setBrush(self.handle_idle_brush) self.top_handle.setPen(self.handle_idle_pen) self.mid_handle.setBrush(self.handle_idle_brush) self.mid_handle.setPen(self.handle_idle_pen) self.bottom_handle.setBrush(self.handle_idle_brush) self.bottom_handle.setPen(self.handle_idle_pen) self.hovered_handle: Optional[QGraphicsRectItem] = None self.handles = [self.top_handle, self.mid_handle, self.bottom_handle] self.link_button = Button("link", "Link to...", parent=self) self.link_button.set_base_color([ButtonColor.GREEN]) self.link_button.set_height(12) self.link_button.set_label("Link", direction="vertical") self.link_button.fit_to_contents() self.link_button.clicked.connect(lambda: self.link_initiated.emit(self)) self.link_button.setVisible(False) self.link_button.setFlag(QGraphicsObject.ItemIgnoresTransformations, False) self.adjust_line() self.setAcceptHoverEvents(True) self.top_handle.setZValue(4) self.bottom_handle.setZValue(4) self.mid_handle.setZValue(4) self.top_handle.hide() self.mid_handle.hide() self.bottom_handle.hide() self.handle_mouse_offset = QPointF(0, 0) self.available_for_linking = False self.link_source = False self.current_highlight_color: QColor = StickWidget.normal_color self.highlighted = False self.frame_color: Optional[None] = self.normal_color self.is_linked = False self.is_master = True self.selected = False self.measured_height: int = -1 self.current_color = self.normal_color self.show_label = False self.highlight_animation = QPropertyAnimation(self, b"highlight_color") self.highlight_animation.valueChanged.connect(self.handle_highlight_animation_value_changed) self.deleting = False self.update_tooltip() self.show_measurements: bool = False self.proposed_snow_height: int = -1 self.zero_btn = Button("zero_btn", "0", parent=self) self.zero_btn.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) self.zero_btn.setVisible(False) self.zero_btn.setPos(self.boundingRect().center() + QPointF(self.zero_btn.boundingRect().width() * -0.5, self.boundingRect().height() * 0.5)) self.zero_btn.clicked.connect(self.handle_zero) @pyqtSlot() def handle_btn_delete_clicked(self): self.delete_clicked.emit(self.stick) def prepare_for_deleting(self): self.deleting = True self.highlight_animation.stop() self.btn_delete.setParentItem(None) self.scene().removeItem(self.btn_delete) self.btn_delete.deleteLater() def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: Optional[PyQt5.QtWidgets.QWidget] = ...): painter.setPen(QPen(self.current_color, 1.0)) brush = QBrush(self.current_highlight_color) pen = QPen(brush, 4) painter.setPen(pen) if self.highlighted: painter.fillRect(self.boundingRect(), QBrush(self.current_highlight_color)) if self.frame_color is not None and self.mode != StickMode.Edit and self.mode != StickMode.EditDelete: painter.setPen(QPen(self.frame_color, 4)) painter.drawRect(self.boundingRect()) pen = QPen(QColor(0, 255, 0, 255)) pen.setWidth(1.0) pen.setColor(QColor(255, 0, 255, 255)) pen.setStyle(Qt.DotLine) painter.setPen(pen) off = 10 painter.drawLine(self.line.p1() - QPointF(0, off), self.line.p1() + QPointF(0, off)) painter.drawLine(self.line.p1() - QPointF(off, 0), self.line.p1() + QPointF(off, 0)) painter.drawLine(self.line.p2() - QPointF(0, off), self.line.p2() + QPointF(0, off)) painter.drawLine(self.line.p2() - QPointF(off, 0), self.line.p2() + QPointF(off, 0)) pen.setStyle(Qt.SolidLine) pen.setColor(QColor(0, 255, 0, 255)) painter.setPen(pen) if self.mode != StickMode.EditDelete: pen.setWidth(2.0) br = painter.brush() painter.setPen(pen) painter.drawEllipse(self.line.p1(), 10, 10) painter.drawEllipse(self.line.p2(), 10, 10) painter.setBrush(br) if self.mode == StickMode.Measurement and self.proposed_snow_height >= 0: point = QPointF(self.boundingRect().x(), -self.proposed_snow_height + self.line.p2().y()) pen = QPen(QColor(200, 100, 0, 255), 3.0) painter.setPen(pen) painter.drawLine(point, point + QPointF(self.boundingRect().width(), 0.0)) if self.measured_height >= 0: vec = (self.stick.top - self.stick.bottom) / np.linalg.norm(self.stick.top - self.stick.bottom) dist_along_stick = self.measured_height / np.dot(np.array([0.0, -1.0]), vec) point = self.line.p2() + dist_along_stick * QPointF(vec[0], vec[1]) point = QPointF(self.boundingRect().x(), point.y()) pen = QPen(QColor(0, 100, 200, 255), 3.0) painter.setPen(pen) painter.drawLine(point, point + QPointF(self.boundingRect().width(), 0.0)) else: painter.drawLine(self.line.p1(), self.line.p2()) if self.selected: pen.setColor(QColor(255, 125, 0, 255)) pen.setStyle(Qt.DashLine) painter.setPen(pen) painter.drawRect(self.boundingRect().marginsAdded(QMarginsF(5, 5, 5, 5))) if self.show_measurements: painter.fillRect(self.stick_label_text.boundingRect().translated(self.stick_label_text.pos()), QBrush(QColor(0, 0, 0, 120))) def boundingRect(self) -> PyQt5.QtCore.QRectF: return self.gline.boundingRect().united(self.top_handle.boundingRect()).\ united(self.mid_handle.boundingRect()).united(self.bottom_handle.boundingRect()) def set_edit_mode(self, value: bool): if value: self.set_mode(StickMode.EditDelete) else: self.set_mode(StickMode.Display) def set_mode(self, mode: StickMode): if mode == StickMode.Display: self.btn_delete.setVisible(False) self.top_handle.setVisible(False) self.mid_handle.setVisible(False) self.bottom_handle.setVisible(False) self.link_button.setVisible(False) self.available_for_linking = False self.link_source = False self.zero_btn.setVisible(False) self.setVisible(self.stick.is_visible) elif mode == StickMode.EditDelete: self.set_mode(StickMode.Display) self.top_handle.setVisible(True) self.mid_handle.setVisible(True) self.bottom_handle.setVisible(True) self.available_for_linking = False self.link_source = False self.btn_delete.setVisible(True) elif mode == StickMode.LinkSource: self.set_mode(StickMode.Display) self.link_source = True self.available_for_linking = False self.link_button.setPos(self.boundingRect().topLeft()) self.link_button.set_width(int(self.boundingRect().width())) self.link_button.set_button_height(int(self.boundingRect().height())) self.link_button.adjust_text_to_button() elif mode == StickMode.LinkTarget: self.set_mode(StickMode.Display) self.link_source = False self.available_for_linking = True elif mode == StickMode.Edit: self.set_mode(StickMode.EditDelete) self.btn_delete.setVisible(False) elif mode == StickMode.Measurement: self.zero_btn.setVisible(True) self.setVisible(True) self.mode = mode self.update_tooltip() self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): if self.mode != StickMode.EditDelete: return if self.hovered_handle is None: return self.hovered_handle.setBrush(self.handle_press_brush) if self.hovered_handle == self.mid_handle: self.bottom_handle.setBrush(self.handle_press_brush) self.bottom_handle.setPen(self.handle_press_pen) self.bottom_handle.setOpacity(0.5) self.top_handle.setBrush(self.handle_press_brush) self.top_handle.setPen(self.handle_press_pen) self.top_handle.setOpacity(0.5) self.hovered_handle.setPen(self.handle_press_pen) self.hovered_handle.setOpacity(0.5) self.handle_mouse_offset = self.hovered_handle.rect().center() - event.pos() self.btn_delete.setVisible(False) def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): if self.available_for_linking: self.link_accepted.emit(self) return if self.mode == StickMode.Measurement: old_snow = self.stick.snow_height_px self.measured_height = self.proposed_snow_height self.stick.set_snow_height_px(self.proposed_snow_height) if abs(old_snow - self.proposed_snow_height) > 0: self.measurement_corrected.emit(self) self.proposed_snow_height = -1 if self.mode != StickMode.EditDelete and self.mode != StickMode.Edit: return if self.hovered_handle is not None: self.hovered_handle.setBrush(self.handle_hover_brush) self.hovered_handle.setPen(self.handle_idle_pen) self.hovered_handle.setOpacity(1.0) if self.hovered_handle == self.mid_handle: self.bottom_handle.setBrush(self.handle_idle_brush) self.bottom_handle.setPen(self.handle_idle_pen) self.bottom_handle.setOpacity(1.0) self.top_handle.setBrush(self.handle_idle_brush) self.top_handle.setPen(self.handle_idle_pen) self.top_handle.setOpacity(1.0) self.stick_changed.emit(self) self.hovered_handle = None if self.mode == StickMode.EditDelete: self.btn_delete.setVisible(True) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): if self.hovered_handle is None: return if self.hovered_handle == self.top_handle: self.line.setP1((event.pos() + self.handle_mouse_offset).toPoint()) elif self.hovered_handle == self.bottom_handle: self.line.setP2((event.pos() + self.handle_mouse_offset).toPoint()) else: displacement = event.pos() - event.lastPos() self.setPos(self.pos() + displacement) self.adjust_handles() self.adjust_stick() self.scene().update() def set_top(self, pos: QPoint): self.line.setP1(pos) self.adjust_handles() self.adjust_stick() self.scene().update() def set_bottom(self, pos: QPoint): self.line.setP2(pos) self.adjust_handles() self.adjust_stick() self.scene().update() def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): if self.available_for_linking: self.hovered.emit(True, self) elif self.link_source: self.link_button.setVisible(True) self.scene().update() def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): for h in self.handles: h.setBrush(self.handle_idle_brush) self.hovered_handle = None if self.available_for_linking: self.hovered.emit(False, self) self.link_button.setVisible(False) self.proposed_snow_height = -1 self.scene().update() def hoverMoveEvent(self, event: QGraphicsSceneHoverEvent): if self.mode != StickMode.EditDelete and self.mode != StickMode.Edit and self.mode != StickMode.Measurement: return if self.mode == StickMode.Measurement: self.proposed_snow_height = max(self.line.p2().y() - event.pos().y(), 0) self.update() return hovered_handle = list(filter(lambda h: h.rect().contains(event.pos()), self.handles)) if len(hovered_handle) == 0: if self.hovered_handle is not None: self.hovered_handle.setBrush(self.handle_idle_brush) self.hovered_handle = None return if self.hovered_handle is not None and self.hovered_handle != hovered_handle[0]: self.hovered_handle.setBrush(self.handle_idle_brush) self.hovered_handle = hovered_handle[0] if self.hovered_handle == self.top_handle: self.top_handle.setBrush(self.handle_hover_brush) elif self.hovered_handle == self.bottom_handle: self.bottom_handle.setBrush(self.handle_hover_brush) else: self.mid_handle.setBrush(self.handle_hover_brush) self.scene().update() def adjust_stick(self): self.stick.top[0] = self.pos().x() + self.line.p1().x() self.stick.top[1] = self.pos().y() + self.line.p1().y() self.stick.bottom[0] = self.pos().x() + self.line.p2().x() self.stick.bottom[1] = self.pos().y() + self.line.p2().y() def adjust_handles(self): if self.line.p1().y() > self.line.p2().y(): p1, p2 = self.line.p1(), self.line.p2() self.line.setP1(p2) self.line.setP2(p1) if self.hovered_handle is not None: self.hovered_handle.setBrush(self.handle_idle_brush) self.hovered_handle.setPen(self.handle_idle_pen) self.hovered_handle = self.top_handle if self.hovered_handle == self.bottom_handle else self.bottom_handle self.hovered_handle.setBrush(self.handle_press_brush) self.hovered_handle.setPen(self.handle_press_pen) rect = self.top_handle.rect() rect.moveCenter(self.line.p1()) self.top_handle.setRect(rect) rect = self.bottom_handle.rect() rect.moveCenter(self.line.p2()) self.bottom_handle.setRect(rect) rect = self.mid_handle.rect() rect.moveCenter(self.line.center()) self.mid_handle.setRect(rect) self.btn_delete.setPos(self.top_handle.rect().center() - QPointF(self.btn_delete.boundingRect().width() / 2, self.btn_delete.boundingRect().height() + self.top_handle.boundingRect().height() / 2)) def set_available_for_linking(self, available: bool): self.available_for_linking = available def set_is_link_source(self, is_source: bool): self.link_source = is_source self.link_button.setPos(self.boundingRect().topLeft()) self.link_button.set_width(int(self.boundingRect().width())) self.link_button.set_button_height(int(self.boundingRect().height())) self.link_button.adjust_text_to_button() def set_frame_color(self, color: Optional[QColor]): self.frame_color = color if color is not None else self.normal_color self.update() def set_is_linked(self, value: bool): self.is_linked = value if not self.is_linked: self.set_frame_color(None) if self.available_for_linking: self.highlight(QColor(0, 255, 0, 100)) else: self.highlight(None) self.update_tooltip() def adjust_line(self): self.setPos(QPointF(0.5 * (self.stick.top[0] + self.stick.bottom[0]), 0.5 * (self.stick.top[1] + self.stick.bottom[1]))) vec = 0.5 * (self.stick.top - self.stick.bottom) self.line.setP1(QPointF(vec[0], vec[1])) self.line.setP2(-self.line.p1()) self.gline.setLine(self.line) self.adjust_handles() self.stick_label_text.setPos(self.line.p1() - QPointF(0.5 * self.stick_label_text.boundingRect().width(), 1.3 * self.stick_label_text.boundingRect().height())) self.update() def set_selected(self, selected: bool): self.selected = selected self.update() def is_selected(self) -> bool: return self.selected def set_snow_height(self, height: int): self.measured_height = height self.update() def border_normal(self): self.current_color = self.normal_color self.update() def border_positive(self): self.current_color = self.positive_color self.update() def border_negative(self): self.current_color = self.negative_color self.update() @pyqtProperty(QColor) def highlight_color(self) -> QColor: return self.current_highlight_color @highlight_color.setter def highlight_color(self, color: QColor): self.current_highlight_color = color def highlight(self, color: Optional[QColor], animated: bool = False): self.highlighted = color is not None if not animated or color is None: self.highlight_animation.stop() self.current_highlight_color = self.normal_color if color is None else color self.update() return self.highlight_animation.setStartValue(color) self.highlight_animation.setEndValue(color) self.highlight_animation.setKeyValueAt(0.5, color.darker()) self.highlight_animation.setDuration(2000) self.highlight_animation.setLoopCount(-1) self.highlight_animation.start() def handle_link_button_hovered(self, btn: Dict[str, Any]): self.link_button.setVisible(btn['hovered']) def handle_highlight_animation_value_changed(self, new: QColor): if not self.deleting: self.update(self.boundingRect().marginsAdded(QMarginsF(10, 10, 10, 10))) def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: self.right_clicked.emit({'stick_widget': self}) def set_stick_label(self, label: str): self.stick.label = label self.stick_label_text.setText(label) self.update_tooltip() self.update() def get_stick_label(self) -> str: return self.stick.label def get_stick_length_cm(self) -> int: return self.stick.length_cm def set_stick_length_cm(self, length: int): self.stick.length_cm = length self.update_tooltip() self.update() def update_tooltip(self): if self.mode != StickMode.Display or self.mode == StickMode.Measurement: self.setToolTip("") return snow_txt = "Snow height: " if self.stick.snow_height_px >= 0: snow_txt += str(self.stick.snow_height_cm) + " cm" self.stick_label_text.setText(str(self.stick.snow_height_cm)) else: snow_txt = "not measured" self.stick_label_text.setVisible(False) self.stick_label_text.setText(self.stick.label) self.stick_label_text.setVisible(True) stick_view_text = '' role = '' if self.stick.alternative_view is not None: alt_view = self.stick.alternative_view role = " - primary" alt = "Secondary" if not self.stick.primary: role = " - secondary" alt = "Primary" stick_view_text = f'\n{alt} view: {alt_view.label} in {alt_view.camera_folder.name}\n' mark = '*' if self.stick.determines_quality else '' self.setToolTip(f'{mark}{self.stick.label}{role}{stick_view_text}\nLength: {self.stick.length_cm} cm\n{snow_txt}') def set_stick(self, stick: Stick): self.reset_d_btns() self.stick = stick self.adjust_line() self.adjust_handles() self.set_snow_height(stick.snow_height_px) self.update_tooltip() self.set_show_measurements(self.show_measurements) if self.mode == StickMode.Measurement: self.set_frame_color(QColor(200, 100, 0, 100) if not self.stick.is_visible else None) self.setVisible(True) self.clearly_visible_btn.setVisible(not self.stick.is_visible) else: self.setVisible(self.stick.is_visible) def set_show_measurements(self, show: bool): self.show_measurements = show if self.show_measurements: self.stick_label_text.setText(str(self.stick.snow_height_cm) if self.stick.snow_height_cm >= 0 else "n/a") else: self.stick_label_text.setText(self.stick.label) self.update() def handle_zero(self): self.measured_height = 0 self.stick.set_snow_height_px(0) self.measurement_corrected.emit(self) def reset_d_btns(self): self.zero_btn.set_default_state()
def refresh(self): if not self._mdlPlots or not self._mdlOutline or not self._mdlPersos: pass LINE_HEIGHT = 18 SPACING = 3 TEXT_WIDTH = self.sldTxtSize.value() CIRCLE_WIDTH = 10 LEVEL_HEIGHT = 12 s = self.scene s.clear() # Get Max Level (max depth) root = self._mdlOutline.rootItem def maxLevel(item, level=0, max=0): if level > max: max = level for c in item.children(): m = maxLevel(c, level + 1) if m > max: max = m return max MAX_LEVEL = maxLevel(root) # Generate left entries # (As of now, plot only) plotsID = self._mdlPlots.getPlotsByImportance() trackedItems = [] fm = QFontMetrics(s.font()) max_name = 0 for importance in plotsID: for ID in importance: name = self._mdlPlots.getPlotNameByID(ID) ref = references.plotReference(ID, searchable=True) trackedItems.append((ID, ref, name)) max_name = max(fm.width(name), max_name) ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING ) TITLE_WIDTH = max_name + 2 * SPACING # Add Folders and Texts outline = OutlineRect(0, 0, 0, ROWS_HEIGHT + SPACING + MAX_LEVEL * LEVEL_HEIGHT) s.addItem(outline) outline.setPos(TITLE_WIDTH + SPACING, 0) refCircles = [] # a list of all references, to be added later on the lines # A Function to add a rect with centered elided text def addRectText(x, w, parent, text="", level=0, tooltip=""): deltaH = LEVEL_HEIGHT if level else 0 r = OutlineRect(0, 0, w, parent.rect().height()-deltaH, parent, title=text) r.setPos(x, deltaH) txt = QGraphicsSimpleTextItem(text, r) f = txt.font() f.setPointSize(8) fm = QFontMetricsF(f) elidedText = fm.elidedText(text, Qt.ElideMiddle, w) txt.setFont(f) txt.setText(elidedText) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) txt.setY(0) return r # A function to returns an item's width, by counting its children def itemWidth(item): if item.isFolder(): r = 0 for c in item.children(): r += itemWidth(c) return r or TEXT_WIDTH else: return TEXT_WIDTH def listItems(item, rect, level=0): delta = 0 for child in item.children(): w = itemWidth(child) if child.isFolder(): parent = addRectText(delta, w, rect, child.title(), level, tooltip=child.title()) parent.setToolTip(references.tooltip(references.textReference(child.ID()))) listItems(child, parent, level + 1) else: rectChild = addRectText(delta, TEXT_WIDTH, rect, "", level, tooltip=child.title()) rectChild.setToolTip(references.tooltip(references.textReference(child.ID()))) # Find tracked references in that scene (or parent folders) for ID, ref, name in trackedItems: result = [] c = child while c: result += references.findReferencesTo(ref, c, recursive=False) c = c.parent() if result: ref2 = result[0] # Create a RefCircle with the reference c = RefCircle(TEXT_WIDTH / 2, - CIRCLE_WIDTH / 2, CIRCLE_WIDTH, ID=ref2) # Store it, with the position of that item, to display it on the line later on refCircles.append((ref, c, rect.mapToItem(outline, rectChild.pos()))) delta += w listItems(root, outline) OUTLINE_WIDTH = itemWidth(root) # Add Plots i = 0 itemsRect = s.addRect(0, 0, 0, 0) itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING) for ID, ref, name in trackedItems: color = randomColor() # Rect r = QGraphicsRectItem(0, 0, TITLE_WIDTH, LINE_HEIGHT, itemsRect) r.setPen(QPen(Qt.NoPen)) r.setBrush(QBrush(color)) r.setPos(0, i * LINE_HEIGHT + i * SPACING) i += 1 # Text txt = QGraphicsSimpleTextItem(name, r) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) # Line line = PlotLine(0, 0, OUTLINE_WIDTH + SPACING, 0) line.setPos(TITLE_WIDTH, r.mapToScene(r.rect().center()).y()) s.addItem(line) line.setPen(QPen(color, 5)) line.setToolTip(self.tr("Plot: ") + name) # We add the circles / references to text, on the line for ref2, circle, pos in refCircles: if ref2 == ref: circle.setParentItem(line) circle.setPos(pos.x(), 0) # self.view.fitInView(0, 0, TOTAL_WIDTH, i * LINE_HEIGHT, Qt.KeepAspectRatioByExpanding) # KeepAspectRatio self.view.setSceneRect(0, 0, 0, 0)
class EdgeItem(GraphItem): _qt_pen_styles = { 'dashed': Qt.DashLine, 'dotted': Qt.DotLine, 'solid': Qt.SolidLine, } def __init__(self, highlight_level, spline, label_center, label, from_node, to_node, parent=None, penwidth=1, edge_color=None, style='solid'): super(EdgeItem, self).__init__(highlight_level, parent) self.from_node = from_node self.from_node.add_outgoing_edge(self) self.to_node = to_node self.to_node.add_incoming_edge(self) self._default_edge_color = self._COLOR_BLACK if edge_color is not None: self._default_edge_color = edge_color self._default_text_color = self._COLOR_BLACK self._default_color = self._COLOR_BLACK self._text_brush = QBrush(self._default_color) self._shape_brush = QBrush(self._default_color) if style in ['dashed', 'dotted']: self._shape_brush = QBrush(Qt.transparent) self._label_pen = QPen() self._label_pen.setColor(self._default_text_color) self._label_pen.setJoinStyle(Qt.RoundJoin) self._edge_pen = QPen(self._label_pen) self._edge_pen.setWidth(penwidth) self._edge_pen.setColor(self._default_edge_color) self._edge_pen.setStyle(self._qt_pen_styles.get(style, Qt.SolidLine)) self._sibling_edges = set() self._label = None if label is not None: self._label = QGraphicsSimpleTextItem(label) self._label.setFont(GraphItem._LABEL_FONT) label_rect = self._label.boundingRect() label_rect.moveCenter(label_center) self._label.setPos(label_rect.x(), label_rect.y()) self._label.hoverEnterEvent = self._handle_hoverEnterEvent self._label.hoverLeaveEvent = self._handle_hoverLeaveEvent self._label.setAcceptHoverEvents(True) # spline specification according to # http://www.graphviz.org/doc/info/attrs.html#k:splineType coordinates = spline.split(' ') # extract optional end_point end_point = None if (coordinates[0].startswith('e,')): parts = coordinates.pop(0)[2:].split(',') end_point = QPointF(float(parts[0]), -float(parts[1])) # extract optional start_point if (coordinates[0].startswith('s,')): parts = coordinates.pop(0).split(',') # first point parts = coordinates.pop(0).split(',') point = QPointF(float(parts[0]), -float(parts[1])) path = QPainterPath(point) while len(coordinates) > 2: # extract triple of points for a cubic spline parts = coordinates.pop(0).split(',') point1 = QPointF(float(parts[0]), -float(parts[1])) parts = coordinates.pop(0).split(',') point2 = QPointF(float(parts[0]), -float(parts[1])) parts = coordinates.pop(0).split(',') point3 = QPointF(float(parts[0]), -float(parts[1])) path.cubicTo(point1, point2, point3) self._arrow = None if end_point is not None: # draw arrow self._arrow = QGraphicsPolygonItem() polygon = QPolygonF() polygon.append(point3) offset = QPointF(end_point - point3) corner1 = QPointF(-offset.y(), offset.x()) * 0.35 corner2 = QPointF(offset.y(), -offset.x()) * 0.35 polygon.append(point3 + corner1) polygon.append(end_point) polygon.append(point3 + corner2) self._arrow.setPolygon(polygon) self._arrow.hoverEnterEvent = self._handle_hoverEnterEvent self._arrow.hoverLeaveEvent = self._handle_hoverLeaveEvent self._arrow.setAcceptHoverEvents(True) self._path = QGraphicsPathItem(parent) self._path.setPath(path) self.addToGroup(self._path) self.set_node_color() self.set_label_color() def add_to_scene(self, scene): scene.addItem(self) if self._label is not None: scene.addItem(self._label) if self._arrow is not None: scene.addItem(self._arrow) def setToolTip(self, tool_tip): super(EdgeItem, self).setToolTip(tool_tip) if self._label is not None: self._label.setToolTip(tool_tip) if self._arrow is not None: self._arrow.setToolTip(tool_tip) def add_sibling_edge(self, edge): self._sibling_edges.add(edge) def set_node_color(self, color=None): if color is None: self._label_pen.setColor(self._default_text_color) self._text_brush.setColor(self._default_color) if self._shape_brush.isOpaque(): self._shape_brush.setColor(self._default_edge_color) self._edge_pen.setColor(self._default_edge_color) else: self._label_pen.setColor(color) self._text_brush.setColor(color) if self._shape_brush.isOpaque(): self._shape_brush.setColor(color) self._edge_pen.setColor(color) self._path.setPen(self._edge_pen) if self._arrow is not None: self._arrow.setBrush(self._shape_brush) self._arrow.setPen(self._edge_pen) def set_label_color(self, color=None): if color is None: self._label_pen.setColor(self._default_text_color) else: self._label_pen.setColor(color) if self._label is not None: self._label.setBrush(self._text_brush) self._label.setPen(self._label_pen) def _handle_hoverEnterEvent(self, event): # hovered edge item in red self.set_node_color(self._COLOR_RED) self.set_label_color(self._COLOR_RED) if self._highlight_level > 1: if self.from_node != self.to_node: # from-node in blue self.from_node.set_node_color(self._COLOR_BLUE) # to-node in green self.to_node.set_node_color(self._COLOR_GREEN) else: # from-node/in-node in teal self.from_node.set_node_color(self._COLOR_TEAL) self.to_node.set_node_color(self._COLOR_TEAL) if self._highlight_level > 2: # sibling edges in orange for sibling_edge in self._sibling_edges: sibling_edge.set_node_color(self._COLOR_ORANGE) def _handle_hoverLeaveEvent(self, event): self.set_node_color() self.set_label_color() if self._highlight_level > 1: self.from_node.set_node_color() self.to_node.set_node_color() if self._highlight_level > 2: for sibling_edge in self._sibling_edges: sibling_edge.set_node_color()
class ModuleItem(UMLItem): """ Class implementing a module item. """ ItemType = "module" def __init__(self, model=None, x=0, y=0, rounded=False, parent=None, scene=None): """ Constructor @param model module model containing the module data (ModuleModel) @param x x-coordinate (integer) @param y y-coordinate (integer) @keyparam rounded flag indicating a rounded corner (boolean) @keyparam parent reference to the parent object (QGraphicsItem) @keyparam scene reference to the scene object (QGraphicsScene) """ UMLItem.__init__(self, model, x, y, rounded, parent) scene.addItem(self) if self.model: self.__createTexts() self.__calculateSize() def __createTexts(self): """ Private method to create the text items of the module item. """ if self.model is None: return boldFont = QFont(self.font) boldFont.setBold(True) classes = self.model.getClasses() x = self.margin + self.rect().x() y = self.margin + self.rect().y() self.header = QGraphicsSimpleTextItem(self) self.header.setFont(boldFont) self.header.setText(self.model.getName()) self.header.setPos(x, y) y += self.header.boundingRect().height() + self.margin if classes: txt = "\n".join(classes) else: txt = " " self.classes = QGraphicsSimpleTextItem(self) self.classes.setFont(self.font) self.classes.setText(txt) self.classes.setPos(x, y) def __calculateSize(self): """ Private method to calculate the size of the module item. """ if self.model is None: return width = self.header.boundingRect().width() height = self.header.boundingRect().height() if self.classes: width = max(width, self.classes.boundingRect().width()) height = height + self.classes.boundingRect().height() self.setSize(width + 2 * self.margin, height + 2 * self.margin) def setModel(self, model): """ Public method to set the module model. @param model module model containing the module data (ModuleModel) """ self.scene().removeItem(self.header) self.header = None if self.classes: self.scene().removeItem(self.classes) self.meths = None self.model = model self.__createTexts() self.__calculateSize() def paint(self, painter, option, widget=None): """ Public method to paint the item in local coordinates. @param painter reference to the painter object (QPainter) @param option style options (QStyleOptionGraphicsItem) @param widget optional reference to the widget painted on (QWidget) """ pen = self.pen() if (option.state & QStyle.State_Selected) == \ QStyle.State(QStyle.State_Selected): pen.setWidth(2) else: pen.setWidth(1) painter.setPen(pen) painter.setBrush(self.brush()) painter.setFont(self.font) offsetX = self.rect().x() offsetY = self.rect().y() w = self.rect().width() h = self.rect().height() painter.drawRect(offsetX, offsetY, w, h) y = self.margin + self.header.boundingRect().height() painter.drawLine(offsetX, offsetY + y, offsetX + w - 1, offsetY + y) self.adjustAssociations() def buildItemDataString(self): """ Public method to build a string to persist the specific item data. This string must start with ", " and should be built like "attribute=value" with pairs separated by ", ". value must not contain ", " or newlines. @return persistence data (string) """ entries = [ "name={0}".format(self.model.getName()), ] classes = self.model.getClasses() if classes: entries.append("classes={0}".format("||".join(classes))) return ", " + ", ".join(entries) def parseItemDataString(self, version, data): """ Public method to parse the given persistence data. @param version version of the data (string) @param data persisted data to be parsed (string) @return flag indicating success (boolean) """ parts = data.split(", ") if len(parts) < 1: return False name = "" classes = [] for part in parts: key, value = part.split("=", 1) if key == "name": name = value.strip() elif key == "classes": classes = value.strip().split("||") else: return False self.model = ModuleModel(name, classes) self.__createTexts() self.__calculateSize() return True
class QtNode(QGraphicsWidget): def __init__(self, node, view): super(QtNode, self).__init__() self._spacingConstant = 5.0 self._roundness = 3 self._labelColor = QColor(255, 255, 255) self._label = QGraphicsSimpleTextItem(self) self._label.setBrush(self._labelColor) self._label.setText(node.name) self._selectedColor = QColor(255, 255, 255) self._shapePen = QPen(Qt.NoPen) self._shapePen.setColor(self._selectedColor) self._shapePen.setWidthF(1.5) self._brush = QBrush(QColor(*COLOUR_THEMES[node.node_type])) self._dropShadowEffect = QGraphicsDropShadowEffect() self.setGraphicsEffect(self._dropShadowEffect) self._dropShadowEffect.setOffset(0.0, 10.0) self._dropShadowEffect.setBlurRadius(8.0) self._dropShadowEffect.setColor(QColor(0, 0, 0, 50)) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setAcceptHoverEvents(True) self.setToolTip(node.tooltip) self._name = node.name self._node = node self._view = weakref.ref(view) self._busy = False self._socketRows = OrderedDict() # Build IO pin socket rows for pin_name in node.pin_order: if pin_name in node.inputs: pin = node.inputs[pin_name] else: pin = node.outputs[pin_name] socket_row = SocketRow(self, pin) self._socketRows[pin_name] = socket_row self.updateLayout() def node(self): return self._node def view(self): return self._view() def name(self): return self._name def setName(self, name): self._name = name self._label.setText(name) self.updateLayout() def labelColor(self): return self._labelColor def onDeleted(self): if self.isSelected(): self.setSelected(False) for socket_row in self._socketRows.values(): socket_row.onDeleted() self._socketRows.clear() def hoverEnterEvent(self, event): self.view().guiOnHoverEnter(self) def hoverLeaveEvent(self, event): self.view().guiOnHoverExit(self) def itemChange(self, change, value): if change == QGraphicsItem.ItemPositionHasChanged: for socket_row in self._socketRows.values(): socket_row.socket().updateConnectionPositions() # Move node if not self._busy: self._busy = True self.view().guiOnMoved(self) self._busy = False elif change == QGraphicsItem.ItemSelectedHasChanged: self.onSelected() return QGraphicsItem.itemChange(self, change, value) def contextMenuEvent(self, event): self.view().guiOnNodeRightClick(self, event) def onSelected(self): if self.isSelected(): self._shapePen.setStyle(Qt.SolidLine) self.view().guiOnNodeSelected(self) else: self._shapePen.setStyle(Qt.NoPen) self.view().guiOnNodeDeselected(self) def paint(self, painter, option, widget): shape = QPainterPath() shape.addRoundedRect(self.rect(), self._roundness, self._roundness) painter.setPen(self._shapePen) painter.setBrush(self._brush) painter.drawPath(shape) def setPos(self, *pos): if len(pos) == 1: point = QPointF(pos[0]) else: point = QPointF(*pos) self._lastPos = point QGraphicsWidget.setPos(self, point) def mouseDoubleClickEvent(self, event): pass def mousePressEvent(self, event): if event.button() == Qt.RightButton: pass else: QGraphicsWidget.mousePressEvent(self, event) def mouseReleaseEvent(self, event): self.view().guiOnFinishedMove() QGraphicsWidget.mouseReleaseEvent(self, event) def mouseMoveEvent(self, event): QGraphicsWidget.mouseMoveEvent(self, event) def dragMoveEvent(self, *args, **kwargs): pass def getSocketRow(self, name): return self._socketRows[name] def refreshSocketRows(self): for socket_row in self._socketRows.values(): socket_row.refresh() def updateLayout(self): label_width = self._label.boundingRect().width() width = label_width y_pos = self._label.boundingRect().bottom() + self._spacingConstant for socket_row in self._socketRows.values(): if socket_row.isVisible(): socket_row.updateLayout() socket_row.setPos(self._spacingConstant, y_pos) height = socket_row.boundingRect().height() y_pos += height attributeWidth = socket_row.boundingRect().width() if attributeWidth > width: width = attributeWidth for socket_row in self._socketRows.values(): if socket_row.isVisible(): hook = socket_row.socket() if hook.isOutput(): hook.setPos(width - hook.boundingRect().width(), hook.pos().y()) width = self._spacingConstant + width + self._spacingConstant self._label.setPos((width - label_width) / 2.0, self._spacingConstant) self.resize(width, y_pos + self._spacingConstant) self.update()
class CameraView(QGraphicsObject): font: QFont = QFont("monospace", 16) stick_link_requested = pyqtSignal(StickWidget) stick_context_menu = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject') stick_widgets_out_of_sync = pyqtSignal('PyQt_PyObject') visibility_toggled = pyqtSignal() synchronize_clicked = pyqtSignal('PyQt_PyObject') previous_photo_clicked = pyqtSignal('PyQt_PyObject') next_photo_clicked = pyqtSignal('PyQt_PyObject') sync_confirm_clicked = pyqtSignal('PyQt_PyObject') sync_cancel_clicked = pyqtSignal('PyQt_PyObject') first_photo_clicked = pyqtSignal('PyQt_PyObject') enter_pressed = pyqtSignal() def __init__(self, scale: float, parent: Optional[QGraphicsItem] = None): QGraphicsObject.__init__(self, parent) self.current_highlight_color = QColor(0, 0, 0, 0) self.current_timer = -1 self.scaling = scale self.pixmap = QGraphicsPixmapItem(self) self.stick_widgets: List[StickWidget] = [] self.link_cam_text = QGraphicsSimpleTextItem("Link camera...", self) self.link_cam_text.setZValue(40) self.link_cam_text.setVisible(False) self.link_cam_text.setFont(CameraView.font) self.link_cam_text.setPos(0, 0) self.link_cam_text.setPen(QPen(QColor(255, 255, 255, 255))) self.link_cam_text.setBrush(QBrush(QColor(255, 255, 255, 255))) self.show_add_buttons = False self.camera = None self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.show_stick_widgets = False self.setAcceptHoverEvents(True) self.stick_edit_mode = False self.original_pixmap = self.pixmap.pixmap() self.hovered = False self.mode = 0 # TODO make enum Mode self.click_handler = None self.double_click_handler: Callable[[int, int], None] = None self.stick_widget_mode = StickMode.Display self.highlight_animation = QPropertyAnimation(self, b"highlight_color") self.highlight_animation.setEasingCurve(QEasingCurve.Linear) self.highlight_animation.valueChanged.connect( self.handle_highlight_color_changed) self.highlight_rect = QGraphicsRectItem(self) self.highlight_rect.setZValue(4) self.highlight_rect.setPen(QPen(QColor(0, 0, 0, 0))) self.title_btn = Button('btn_title', '', parent=self) self.title_btn.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) self.title_btn.setZValue(5) self.title_btn.setVisible(False) self.sticks_without_width: List[Stick] = [] self.current_image_name: str = '' self.control_widget = ControlWidget(parent=self) self.control_widget.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) self.control_widget.setVisible(True) self._connect_control_buttons() self.image_available = True self.blur_eff = QGraphicsBlurEffect() self.blur_eff.setBlurRadius(5.0) self.blur_eff.setEnabled(False) self.pixmap.setGraphicsEffect(self.blur_eff) self.overlay_message = QGraphicsSimpleTextItem('not available', parent=self) font = self.title_btn.font font.setPointSize(48) self.overlay_message.setFont(font) self.overlay_message.setBrush(QBrush(QColor(200, 200, 200, 200))) self.overlay_message.setPen(QPen(QColor(0, 0, 0, 200), 2.0)) self.overlay_message.setVisible(False) self.overlay_message.setZValue(6) self.stick_box = QGraphicsRectItem(parent=self) self.stick_box.setFlag(QGraphicsItem.ItemIsMovable, True) self.stick_box.setVisible(False) self.stick_box_start_pos = QPoint() def _connect_control_buttons(self): self.control_widget.synchronize_btn.clicked.connect( lambda: self.synchronize_clicked.emit(self)) self.control_widget.prev_photo_btn.clicked.connect( lambda: self.previous_photo_clicked.emit(self)) self.control_widget.next_photo_btn.clicked.connect( lambda: self.next_photo_clicked.emit(self)) self.control_widget.accept_btn.clicked.connect( lambda: self.sync_confirm_clicked.emit(self)) self.control_widget.cancel_btn.clicked.connect( lambda: self.sync_cancel_clicked.emit(self)) self.control_widget.first_photo_btn.clicked.connect( lambda: self.first_photo_clicked.emit(self)) def paint(self, painter: QPainter, option: PyQt5.QtWidgets.QStyleOptionGraphicsItem, widget: QWidget): if self.pixmap.pixmap().isNull(): return painter.setRenderHint(QPainter.Antialiasing, True) if self.show_stick_widgets: brush = QBrush(QColor(255, 255, 255, 100)) painter.fillRect(self.boundingRect(), brush) if self.mode and self.hovered: pen = QPen(QColor(0, 125, 200, 255)) pen.setWidth(4) painter.setPen(pen) def boundingRect(self) -> PyQt5.QtCore.QRectF: return self.pixmap.boundingRect().united( self.title_btn.boundingRect().translated(self.title_btn.pos())) def initialise_with(self, camera: Camera): if self.camera is not None: self.camera.stick_added.disconnect(self.handle_stick_created) self.camera.sticks_added.disconnect(self.handle_sticks_added) self.camera.stick_removed.disconnect(self.handle_stick_removed) self.camera.sticks_removed.disconnect(self.handle_sticks_removed) self.camera.stick_changed.disconnect(self.handle_stick_changed) self.camera = camera self.prepareGeometryChange() self.set_image(camera.rep_image, Path(camera.rep_image_path).name) self.title_btn.set_label(self.camera.folder.name) self.title_btn.set_height(46) self.title_btn.fit_to_contents() self.title_btn.set_width(int(self.boundingRect().width())) self.title_btn.setPos(0, self.boundingRect().height()) self.control_widget.title_btn.set_label(self.camera.folder.name) self.camera.stick_added.connect(self.handle_stick_created) self.camera.sticks_added.connect(self.handle_sticks_added) self.camera.stick_removed.connect(self.handle_stick_removed) self.camera.sticks_removed.connect(self.handle_sticks_removed) self.camera.stick_changed.connect(self.handle_stick_changed) self.control_widget.set_font_height(32) self.control_widget.set_widget_height( self.title_btn.boundingRect().height()) self.control_widget.set_widget_width(int(self.boundingRect().width())) self.control_widget.setPos(0, self.pixmap.boundingRect().height() ) #self.boundingRect().height()) self.control_widget.set_mode('view') self.update_stick_widgets() def set_image(self, img: Optional[np.ndarray] = None, image_name: Optional[str] = None): if img is None: self.show_overlay_message('not available') return self.show_overlay_message(None) self.prepareGeometryChange() barray = QByteArray(img.tobytes()) image = QImage(barray, img.shape[1], img.shape[0], QImage.Format_BGR888) self.original_pixmap = QPixmap.fromImage(image) self.pixmap.setPixmap(self.original_pixmap) self.highlight_rect.setRect(self.boundingRect()) self.current_image_name = image_name def update_stick_widgets(self): stick_length = 60 for stick in self.camera.sticks: sw = StickWidget(stick, self.camera, self) sw.set_mode(self.stick_widget_mode) self.connect_stick_widget_signals(sw) self.stick_widgets.append(sw) stick_length = stick.length_cm self.update_stick_box() self.scene().update() def scale_item(self, factor: float): self.prepareGeometryChange() pixmap = self.original_pixmap.scaledToHeight( int(self.original_pixmap.height() * factor)) self.pixmap.setPixmap(pixmap) self.__update_title() def set_show_stick_widgets(self, value: bool): for sw in self.stick_widgets: sw.setVisible(value) self.scene().update() def hoverEnterEvent(self, e: QGraphicsSceneHoverEvent): self.hovered = True self.scene().update(self.sceneBoundingRect()) def hoverLeaveEvent(self, e: QGraphicsSceneHoverEvent): self.hovered = False self.scene().update(self.sceneBoundingRect()) def mousePressEvent(self, e: QGraphicsSceneMouseEvent): super().mousePressEvent(e) def mouseReleaseEvent(self, e: QGraphicsSceneMouseEvent): if self.mode == 1: self.click_handler(self.camera) def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent): if self.stick_widget_mode == StickMode.EditDelete: x = event.pos().toPoint().x() y = event.pos().toPoint().y() stick = self.camera.create_new_sticks( [(np.array([[x, y - 50], [x, y + 50]]), 3)], self.current_image_name)[ 0] #self.dataset.create_new_stick(self.camera) self.sticks_without_width.append(stick) def set_button_mode(self, click_handler: Callable[[Camera], None], data: str): self.mode = 1 # TODO make a proper ENUM self.click_handler = lambda c: click_handler(c, data) def set_display_mode(self): self.mode = 0 # TODO make a proper ENUM self.click_handler = None def _remove_stick_widgets(self): for sw in self.stick_widgets: sw.setParentItem(None) self.scene().removeItem(sw) sw.deleteLater() self.stick_widgets.clear() def handle_stick_created(self, stick: Stick): if stick.camera_id != self.camera.id: return sw = StickWidget(stick, self.camera, self) sw.set_mode(self.stick_widget_mode) self.connect_stick_widget_signals(sw) self.stick_widgets.append(sw) self.stick_widgets_out_of_sync.emit(self) self.update() def handle_stick_removed(self, stick: Stick): if stick.camera_id != self.camera.id: return stick_widget = next( filter(lambda sw: sw.stick.id == stick.id, self.stick_widgets)) self.disconnect_stick_widget_signals(stick_widget) self.stick_widgets.remove(stick_widget) stick_widget.setParentItem(None) self.scene().removeItem(stick_widget) stick_widget.deleteLater() self.update() def handle_sticks_removed(self, sticks: List[Stick]): if sticks[0].camera_id != self.camera.id: return for stick in sticks: to_remove: StickWidget = None for sw in self.stick_widgets: if sw.stick.id == stick.id: to_remove = sw break self.stick_widgets.remove(to_remove) to_remove.setParentItem(None) if self.scene() is not None: self.scene().removeItem(to_remove) to_remove.deleteLater() self.update() def handle_sticks_added(self, sticks: List[Stick]): if len(sticks) == 0: return if sticks[0].camera_id != self.camera.id: return for stick in sticks: sw = StickWidget(stick, self.camera, self) sw.set_mode(self.stick_widget_mode) self.connect_stick_widget_signals(sw) self.stick_widgets.append(sw) self.update_stick_box() self.stick_widgets_out_of_sync.emit(self) self.update() def connect_stick_widget_signals(self, stick_widget: StickWidget): stick_widget.delete_clicked.connect( self.handle_stick_widget_delete_clicked) stick_widget.stick_changed.connect(self.handle_stick_widget_changed) stick_widget.link_initiated.connect(self.handle_stick_link_initiated) stick_widget.right_clicked.connect( self.handle_stick_widget_context_menu) def disconnect_stick_widget_signals(self, stick_widget: StickWidget): stick_widget.delete_clicked.disconnect( self.handle_stick_widget_delete_clicked) stick_widget.stick_changed.disconnect(self.handle_stick_widget_changed) stick_widget.link_initiated.disconnect( self.handle_stick_link_initiated) stick_widget.right_clicked.disconnect( self.handle_stick_widget_context_menu) def handle_stick_widget_delete_clicked(self, stick: Stick): self.camera.remove_stick(stick) def set_stick_widgets_mode(self, mode: StickMode): self.stick_widget_mode = mode for sw in self.stick_widgets: sw.set_mode(mode) self.set_stick_edit_mode(mode == StickMode.Edit) def handle_stick_widget_changed(self, stick_widget: StickWidget): self.camera.stick_changed.emit(stick_widget.stick) def handle_stick_changed(self, stick: Stick): if stick.camera_id != self.camera.id: return sw = next( filter(lambda _sw: _sw.stick.id == stick.id, self.stick_widgets)) sw.adjust_line() sw.update_tooltip() def handle_stick_link_initiated(self, stick_widget: StickWidget): self.stick_link_requested.emit(stick_widget) def get_top_left(self) -> QPointF: return self.sceneBoundingRect().topLeft() def get_top_right(self) -> QPointF: return self.sceneBoundingRect().topRight() def highlight(self, color: Optional[QColor]): if color is None: self.highlight_animation.stop() self.highlight_rect.setVisible(False) return alpha = color.alpha() color.setAlpha(0) self.highlight_animation.setStartValue(color) self.highlight_animation.setEndValue(color) color.setAlpha(alpha) self.highlight_animation.setKeyValueAt(0.5, color) self.highlight_animation.setDuration(2000) self.highlight_animation.setLoopCount(-1) self.highlight_rect.setPen(QPen(color)) self.highlight_rect.setVisible(True) self.highlight_animation.start() @pyqtProperty(QColor) def highlight_color(self) -> QColor: return self.current_highlight_color @highlight_color.setter def highlight_color(self, color: QColor): self.current_highlight_color = color def handle_highlight_color_changed(self, color: QColor): self.highlight_rect.setBrush(QBrush(color)) self.update() def handle_stick_widget_context_menu(self, sender: Dict[str, StickWidget]): self.stick_context_menu.emit(sender['stick_widget'], self) def show_overlay_message(self, msg: Optional[str]): if msg is None: self.overlay_message.setVisible(False) self.blur_eff.setEnabled(False) return self.overlay_message.setText(msg) self.overlay_message.setPos( self.pixmap.boundingRect().center() - QPointF(0.5 * self.overlay_message.boundingRect().width(), 0.5 * self.overlay_message.boundingRect().height())) self.overlay_message.setVisible(True) self.blur_eff.setEnabled(True) def show_status_message(self, msg: Optional[str]): if msg is None: self.control_widget.set_title_text(self.camera.folder.name) else: self.control_widget.set_title_text(msg) def update_stick_box(self): left = 9000 right = 0 top = 9000 bottom = -1 for stick in self.camera.sticks: left = min(left, min(stick.top[0], stick.bottom[0])) right = max(right, max(stick.top[0], stick.bottom[0])) top = min(top, min(stick.top[1], stick.bottom[1])) bottom = max(bottom, max(stick.top[1], stick.bottom[1])) left -= 100 right += 100 top -= 100 bottom += 100 self.stick_box.setRect(left, top, right - left, bottom - top) pen = QPen(QColor(0, 100, 200, 200)) pen.setWidth(2) pen.setStyle(Qt.DashLine) self.stick_box.setPen(pen) def set_stick_edit_mode(self, is_edit: bool): if is_edit: self.update_stick_box() self.stick_box_start_pos = self.stick_box.pos() for sw in self.stick_widgets: sw.setParentItem(self.stick_box) else: offset = self.stick_box.pos() - self.stick_box_start_pos for sw in self.stick_widgets: stick = sw.stick stick.translate(np.array([int(offset.x()), int(offset.y())])) sw.setParentItem(self) sw.set_stick(stick) self.stick_box.setParentItem(None) self.stick_box = QGraphicsRectItem(self) self.stick_box.setFlag(QGraphicsItem.ItemIsMovable, True) self.stick_box.setVisible(False) self.stick_box.setVisible(is_edit) def keyPressEvent(self, event: QKeyEvent) -> None: pass def keyReleaseEvent(self, event: QKeyEvent) -> None: if event.key() in [Qt.Key_Right, Qt.Key_Tab, Qt.Key_Space]: self.control_widget.next_photo_btn.click_button(True) elif event.key() in [Qt.Key_Left]: self.control_widget.prev_photo_btn.click_button(True) elif event.key() == Qt.Key_S: self.enter_pressed.emit()
def refresh(self): if not self._mdlPlots or not self._mdlOutline or not self._mdlCharacter: return if not self.isVisible(): return LINE_HEIGHT = 18 SPACING = 3 TEXT_WIDTH = self.sldTxtSize.value() CIRCLE_WIDTH = 10 LEVEL_HEIGHT = 12 s = self.scene s.clear() # Get Max Level (max depth) root = self._mdlOutline.rootItem def maxLevel(item, level=0, max=0): if level > max: max = level for c in item.children(): m = maxLevel(c, level + 1) if m > max: max = m return max MAX_LEVEL = maxLevel(root) # Get the list of tracked items (array of references) trackedItems = [] if self.actPlots.isChecked(): trackedItems += self.plotReferences() if self.actCharacters.isChecked(): trackedItems += self.charactersReferences() ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING ) fm = QFontMetrics(s.font()) max_name = 0 for ref in trackedItems: name = references.title(ref) max_name = max(fm.width(name), max_name) TITLE_WIDTH = max_name + 2 * SPACING # Add Folders and Texts outline = OutlineRect(0, 0, 0, ROWS_HEIGHT + SPACING + MAX_LEVEL * LEVEL_HEIGHT) s.addItem(outline) outline.setPos(TITLE_WIDTH + SPACING, 0) refCircles = [] # a list of all references, to be added later on the lines # A Function to add a rect with centered elided text def addRectText(x, w, parent, text="", level=0, tooltip=""): deltaH = LEVEL_HEIGHT if level else 0 r = OutlineRect(0, 0, w, parent.rect().height()-deltaH, parent, title=text) r.setPos(x, deltaH) txt = QGraphicsSimpleTextItem(text, r) f = txt.font() f.setPointSize(8) fm = QFontMetricsF(f) elidedText = fm.elidedText(text, Qt.ElideMiddle, w) txt.setFont(f) txt.setText(elidedText) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) txt.setY(0) return r # A function to returns an item's width, by counting its children def itemWidth(item): if item.isFolder(): r = 0 for c in item.children(): r += itemWidth(c) return r or TEXT_WIDTH else: return TEXT_WIDTH def listItems(item, rect, level=0): delta = 0 for child in item.children(): w = itemWidth(child) if child.isFolder(): parent = addRectText(delta, w, rect, child.title(), level, tooltip=child.title()) parent.setToolTip(references.tooltip(references.textReference(child.ID()))) listItems(child, parent, level + 1) else: rectChild = addRectText(delta, TEXT_WIDTH, rect, "", level, tooltip=child.title()) rectChild.setToolTip(references.tooltip(references.textReference(child.ID()))) # Find tracked references in that scene (or parent folders) for ref in trackedItems: result = [] # Tests if POV scenePOV = False # Will hold true of character is POV of the current text, not containing folder if references.type(ref) == references.CharacterLetter: ID = references.ID(ref) c = child while c: if c.POV() == ID: result.append(c.ID()) if c == child: scenePOV = True c = c.parent() # Search in notes/references c = child while c: result += references.findReferencesTo(ref, c, recursive=False) c = c.parent() if result: ref2 = result[0] # Create a RefCircle with the reference c = RefCircle(TEXT_WIDTH / 2, - CIRCLE_WIDTH / 2, CIRCLE_WIDTH, ID=ref2, important=scenePOV) # Store it, with the position of that item, to display it on the line later on refCircles.append((ref, c, rect.mapToItem(outline, rectChild.pos()))) delta += w listItems(root, outline) OUTLINE_WIDTH = itemWidth(root) # Add Tracked items i = 0 itemsRect = s.addRect(0, 0, 0, 0) itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING) # Set of colors for plots (as long as they don't have their own colors) colors = [ "#D97777", "#AE5F8C", "#D9A377", "#FFC2C2", "#FFDEC2", "#D2A0BC", "#7B0F0F", "#7B400F", "#620C3D", "#AA3939", "#AA6C39", "#882D61", "#4C0000", "#4C2200", "#3D0022", ] for ref in trackedItems: if references.type(ref) == references.CharacterLetter: color = self._mdlCharacter.getCharacterByID(references.ID(ref)).color() else: color = QColor(colors[i % len(colors)]) # Rect r = QGraphicsRectItem(0, 0, TITLE_WIDTH, LINE_HEIGHT, itemsRect) r.setPen(QPen(Qt.NoPen)) r.setBrush(QBrush(color)) r.setPos(0, i * LINE_HEIGHT + i * SPACING) r.setToolTip(references.tooltip(ref)) i += 1 # Text name = references.title(ref) txt = QGraphicsSimpleTextItem(name, r) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) # Line line = PlotLine(0, 0, OUTLINE_WIDTH + SPACING, 0) line.setPos(TITLE_WIDTH, r.mapToScene(r.rect().center()).y()) s.addItem(line) line.setPen(QPen(color, 5)) line.setToolTip(references.tooltip(ref)) # We add the circles / references to text, on the line for ref2, circle, pos in refCircles: if ref2 == ref: circle.setParentItem(line) circle.setPos(pos.x(), 0) # self.view.fitInView(0, 0, TOTAL_WIDTH, i * LINE_HEIGHT, Qt.KeepAspectRatioByExpanding) # KeepAspectRatio self.view.setSceneRect(0, 0, 0, 0)
class Tile(Pixmap): """ Tile class defines on screen tiles """ anim_complete = pyqtSignal() # Signal for completion of animation sheet = None # Sprite sheet def __init__(self, letter, scene, letfile=r"\scrabble_letters.png"): self.alphabet = scene.alphabet if type(self).sheet is None: type(self).sheet = QPixmap(self.alphabet.lang + letfile) # Extract letter tile from sheet, scale to cell size for board image = type(self).sheet.copy(self.alphabet.TILE_POSITIONS[letter][0], self.alphabet.TILE_POSITIONS[letter][1], Cons.TILE_WIDTH, Cons.TILE_HEIGHT) image = image.scaled(Cons.WIDTH, Cons.HEIGHT, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) super(Tile, self).__init__(image) type(self).sheet = None self.letter = letter self.blank_letter = None self.rack_pos = QPointF() self.pos = QPointF() self.txt = None self.scene = scene self.alphabet = self.scene.alphabet self.pixmap_item.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable) self.pixmap_item.setTransform(QTransform()) self.pixmap_item.setAcceptedMouseButtons(Qt.LeftButton) self.pixmap_item.setZValue(1000) self.pixmap_item.letter = letter self.pixmap_item.tile = self self.pixmap_item.hide() # self.add_to_scene(self.scene) self.scene.addItem(self.pixmap_item) self.pos = QPointF(0, 0) self.animation = None self.fade = None # Draw at position (QPoint) on screen def draw_tile(self, position): """ Extract letter tile from sheet, scale to cell size for board draw at (xpos, ypos) """ self.pos = position self.pixmap_item.show() def get_pos(self): """ Return position of tile """ return self.pixmap_item.pos() def set_pos(self, x, y): """ Move tile to position (x, y) """ self.pixmap_item.setPos(x, y) def hand_cursor(self): """ Change cursor to hand cursor """ self.pixmap_item.setCursor(Qt.PointingHandCursor) def reset_cursor(self): """ Change cursor to pointer cursor """ self.pixmap_item.setCursor(Qt.ArrowCursor) def move_tile(self, dur, *args): """ Create move tile animation *args are time fraction, points in path either as (time, QPointF) or (time, x, y) """ if not self.pixmap_item.isVisible(): self.pixmap_item.setPos( QPointF(Cons.WINDOW_SIZE[0] / 2 - Cons.WIDTH / 2, Cons.WINDOW_SIZE[1])) self.pixmap_item.show() animation = QPropertyAnimation(self, b'pos') animation.setDuration(dur) for val in args: if isinstance(val[1], QPointF): point = val[1] else: point = QPointF(val[1], val[2]) animation.setKeyValueAt(val[0], point) self.animation = animation return self.animation def activate(self, activate): """ Accept mouse presses if activate is True """ if activate: self.pixmap_item.setAcceptedMouseButtons(Qt.LeftButton) else: self.pixmap_item.setAcceptedMouseButtons(Qt.NoButton) def dim(self, activate): """ Dim tile if activate is True """ if activate: self.pixmap_item.setOpacity(0.4) else: self.pixmap_item.setOpacity(1) def set_in_board(self): """ Set tile in board No longer moveable """ self.pixmap_item.setAcceptedMouseButtons(Qt.NoButton) def add_letter(self, letter): """ Add small letter to blank tile """ self.txt = QGraphicsSimpleTextItem(letter, self.pixmap_item) self.txt.setFont(QFont("Arial", 14, QFont.DemiBold)) self.txt.setBrush(QBrush(Qt.darkRed)) wd, ht = self.txt.boundingRect().width(), self.txt.boundingRect( ).height() self.txt.setPos((Cons.WIDTH - wd) / 2, (Cons.HEIGHT - ht) / 2) def get_tile(self): """ Move tile from store (bottom centre) to position on rack Used in Group Animation """ return self.move_tile( 1000, (0, Cons.WINDOW_SIZE[0] / 2 - Cons.WIDTH / 2, Cons.WINDOW_SIZE[1]), (0.2, Cons.WINDOW_SIZE[0] / 2 - Cons.WIDTH / 2, self.rack_pos.y()), (1, self.rack_pos)) def return_tile(self): """ Return tile to rack Used in Group Animation """ return self.move_tile(400, (0, self.get_pos()), (1, self.rack_pos)) def remove_tile(self): """ Remove tile from board Used in Group Animation """ return self.move_tile( 1000, (0, self.get_pos()), (0.8, Cons.WINDOW_SIZE[0] / 2 - Cons.WIDTH / 2, self.get_pos().y()), (1, Cons.WINDOW_SIZE[0] / 2 - Cons.WIDTH / 2, Cons.WINDOW_SIZE[1])) def lift_tile(self): """ Used in exchange tiles Lift chosen tile to set position above rack """ self.animation = self.move_tile( 100, (0, self.rack_pos), (1, self.rack_pos + QPointF(0, Cons.TILE_LIFT))) self.animation.start() def drop_tile(self): """ Used in exchange tiles Drop chosen tile back into rack """ self.animation = self.move_tile(100, (0, self.get_pos()), (1, self.rack_pos)) self.animation.start() def name_tile(self, player): """ Used in names screen Move chosen blank tiles to required position """ point = QPointF( Cons.INPUT_NAMES[player][0] + Cons.INPUT_NAMES[player][2], Cons.INPUT_NAMES[player][1] + 50) self.animation = self.move_tile(100, (0, self.get_pos()), (1, point)) self.animation.start() def return_blank(self): """ Used in names screen Return tile to position on board """ self.animation = self.move_tile(400, (0, self.get_pos()), (1, self.rack_pos)) self.animation.start() def unfade(self): """ Fade in letter on blank tile """ self.fade = QPropertyAnimation(self, b'opacity') self.fade.setDuration(2000) self.fade.setStartValue(0) self.fade.setEndValue(1) self.fade.setEasingCurve(QEasingCurve.InOutSine) self.fade.finished.connect(self._fade_end) self.fade.start() def _fade_end(self): """ end of animation """ self.anim_complete.emit() def hide(self): """ Hide tile """ self.pixmap_item.hide() def setZValue(self, val): """ set ZValue for image """ self.pixmap_item.setZValue(val)
class GAction(QtWidgets.QGraphicsPathItem, GTooltipBase): """Base class for all graphical actions.""" def __init__(self, g_data_item, w_data_item, parent=None, eval_gui=None, appending_ports=True): """Initializes GAction. :param g_data_item: Object which holds data describing graphical properties of this GAction. :param w_data_item: Object which holds data about action in workflow. :param parent: Action which holds this subaction: this GAction is inside parent GAction. """ super(GAction, self).__init__(parent) self._width = g_data_item.data(GActionData.WIDTH) self._height = g_data_item.data(GActionData.HEIGHT) self.in_ports = [] self.out_ports = [] self.eval_gui = eval_gui self._hide_name = False self.appending_ports = appending_ports self.setPos(QtCore.QPoint(g_data_item.data(GActionData.X), g_data_item.data(GActionData.Y))) self.setPen(QtGui.QPen(QtCore.Qt.black)) self.setBrush(QtCore.Qt.darkGray) self.setFlag(self.ItemIsMovable) self.setFlag(self.ItemIsSelectable) self.setFlag(self.ItemSendsGeometryChanges) self.setZValue(0.0) self.resize_handle_width = 6 self.type_name = QGraphicsSimpleTextItem(w_data_item.action_name, self) self.type_name.setPos(QtCore.QPoint(self.resize_handle_width, GPort.SIZE / 2)) self.type_name.setBrush(QtCore.Qt.white) self._name = EditableLabel(g_data_item.data(GActionData.NAME), self) self.background = GActionBackground(self) self.background.setZValue(1.0) self.setCacheMode(self.DeviceCoordinateCache) self.g_data_item = g_data_item self.w_data_item = w_data_item self.update_ports() self.level = 0 self.height = self.height self.width = self.width self.progress = 0 def has_const_params(self): if len(self.w_data_item.parameters.parameters) > 0: return self.w_data_item.parameters.parameters[-1].name is not None else: return True def update_ports(self): if len(self.w_data_item.parameters.parameters) > 0: self._add_ports(len(self.w_data_item.arguments), not self.has_const_params()) else: self._add_ports(len(self.w_data_item.arguments)) def __repr__(self): return self.name + "\t" + str(self.level) def hide_name(self, boolean): self._hide_name = boolean if boolean: self._name.setParentItem(None) self._name.hide() else: self._name.setParentItem(self) self._name.show() self._height = self.height - self.inner_area().height() self.position_ports() self.update_gfx() @property def status(self): return self.background.status @status.setter def status(self, status): self.background.status = status self.setBrush(self.background.COLOR_PALETTE[self.status]) self.update() @property def progress(self): return self.background.progress @progress.setter def progress(self, percent): self.background.update_gfx() self.background.progress = percent @property def name(self): return self._name.toPlainText() @name.setter def name(self, name): self._name.setPlainText(name) @property def width(self): return self._width @width.setter def width(self, value): self._width = max(value, self.width_of_ports(), self._name.boundingRect().width() + 2 * self.resize_handle_width, self.type_name.boundingRect().width() + 2 * self.resize_handle_width) self.position_ports() self.update_gfx() # self.resize_handles.update_handles() @property def height(self): return self._height @height.setter def height(self, value): self._height = max(value, self._name.boundingRect().height() + GPort.SIZE + self.type_name.boundingRect().height() + GPort.SIZE) self.position_ports() self.update_gfx() #self.resize_handles.update_handles() def boundingRect(self): return super(GAction, self).boundingRect().united(self.childrenBoundingRect()) def next_actions(self): ret = [] for port in self.out_ports: for conn in port.connections: item = conn.port2.parentItem() if item not in ret: ret.append(item) return ret def previous_actions(self): ret = [] for port in self.in_ports: for conn in port.connections: item = conn.port1.parentItem() if item not in ret: ret.append(item) return ret def name_change(self): self.scene().update() self.width = self.width def name_has_changed(self): if not self.scene().action_name_changed(self.g_data_item, self.name) or self.name == "": return False self.width = self.width self.w_data_item.name(self.name) self.scene().update() return True def width_has_changed(self): self.scene().action_model.width_changed(self.g_data_item, self.width) def height_has_changed(self): self.scene().action_model.height_changed(self.g_data_item, self.height) def get_port(self, input, index): if input: return self.in_ports[index] else: return self.out_ports[index] def _add_ports(self, n_ports, appending=False): for i in range(n_ports): self.add_g_port(True, "Input Port" + str(i)) if appending and self.appending_ports: self.add_g_port(True, "Appending port") self.in_ports[-1].appending_port = True self.add_g_port(False, "Output Port") def inner_area(self): """Returns rectangle of the inner area of GAction.""" return QRectF(self.resize_handle_width, GPort.SIZE / 2 + self.type_name.boundingRect().height() + 4, self.width - 2 * self.resize_handle_width, self.height - GPort.SIZE - self.type_name.boundingRect().height() - 4) def moveBy(self, dx, dy): super(GAction, self).moveBy(dx, dy) self.scene().move(self.g_data_item, self.x() + dx, self.y() + dy) def mousePressEvent(self, press_event): super(GAction, self).mousePressEvent(press_event) self.setCursor(QtCore.Qt.ClosedHandCursor) if press_event.button() == Qt.RightButton: self.setSelected(True) def mouseReleaseEvent(self, release_event): super(GAction, self).mouseReleaseEvent(release_event) self.setCursor(QtCore.Qt.OpenHandCursor) temp = release_event.buttonDownScenePos(Qt.LeftButton) temp2 = release_event.pos() if release_event.buttonDownScenePos(Qt.LeftButton) != self.mapToScene(release_event.pos()): for item in self.scene().selectedItems(): if self.scene().is_action(item): self.scene().move(item.g_data_item, item.x(), item.y()) def mouseDoubleClickEvent(self, event): if self.eval_gui is None: if self._name.contains(self.mapToItem(self._name, event.pos())): self._name.mouseDoubleClickEvent(event) else: self.eval_gui.double_click(self) def itemChange(self, change_type, value): """Update all connections which are attached to this action.""" if change_type == self.ItemPositionHasChanged: for port in self.ports(): for conn in port.connections: conn.update_gfx() ''' elif change_type == self.ItemParentChange: self.setPos(self.mapToItem(value, self.mapToScene(self.pos()))) ''' return super(GAction, self).itemChange(change_type, value) def paint(self, painter, item, widget=None): """Update model of this GAction if necessary.""" #self.setBrush(self.background.COLOR_PALETTE[self.status]) super(GAction, self).paint(painter, item, widget) def paint_pixmap(self): progress = self.progress status = self.status self.progress = 0 self.status = ActionStatus.IDLE rect = self.boundingRect() pixmap = QPixmap(rect.size().toSize()) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing, True) painter.translate(-rect.topLeft()) for child in self.childItems(): if child.flags() & QGraphicsItem.ItemStacksBehindParent: painter.save() painter.translate(child.mapToParent(self.pos())) child.paint(painter, QStyleOptionGraphicsItem(), None) painter.restore() self.paint(painter, QStyleOptionGraphicsItem()) for child in self.childItems(): if not child.flags() & QGraphicsItem.ItemStacksBehindParent: painter.save() painter.translate(child.mapToParent(self.pos())) child.paint(painter, QStyleOptionGraphicsItem(), None) painter.restore() painter.end() self.progress = progress self.status = status return pixmap def update_gfx(self): """Updates model of the GAction.""" self.prepareGeometryChange() p = QtGui.QPainterPath() p.addRoundedRect(QtCore.QRectF(0, 0, self.width, self.height), 6, 6) if not self._hide_name: p.addRoundedRect(self.inner_area(), 4, 4) self.setPath(p) self.update() self.background.update_gfx() def add_g_port(self, is_input, name=""): """Adds a port to this GAction. :param is_input: Decides if the new port will be input or output. """ if is_input: self.in_ports.append(GInputPort(len(self.in_ports), QtCore.QPoint(0, 0), name, self)) else: self.out_ports.clear() self.out_ports.append(GOutputPort(len(self.out_ports), QtCore.QPoint(0, 0), name, self)) self.width = self.width def position_ports(self): if len(self.in_ports): space = self.width / (len(self.in_ports)) for i in range(len(self.in_ports)): self.in_ports[i].setPos(QtCore.QPoint((i + 0.5) * space - GPort.RADIUS, -GPort.RADIUS)) if len(self.out_ports): space = self.width / (len(self.out_ports)) for i in range(len(self.out_ports)): self.out_ports[i].setPos(QtCore.QPoint((i + 0.5) * space - GPort.RADIUS, self.height - GPort.RADIUS)) def width_of_ports(self): return max(len(self.in_ports) * GPort.SIZE, len(self.out_ports) * GPort.SIZE) def ports(self): """Returns input and output ports.""" return self.in_ports + self.out_ports def get_arg_action(self, arg): index = self.w_data_item.arguments.index(arg) if len(self.in_ports[index].connections) == 1: return self.in_ports[index].connections[0].port1.parentItem() else: return None
class NodeItem(GraphItem): def __init__(self, highlight_level, bounding_box, label, shape, color=None, parent=None, label_pos=None, tooltip=None): super(NodeItem, self).__init__(highlight_level, parent) self._default_color = self._COLOR_BLACK if color is None else color self._brush = QBrush(self._default_color) self._label_pen = QPen() self._label_pen.setColor(self._default_color) self._label_pen.setJoinStyle(Qt.RoundJoin) self._ellipse_pen = QPen(self._label_pen) self._ellipse_pen.setWidth(1) self._incoming_edges = set() self._outgoing_edges = set() self.parse_shape(shape, bounding_box) self.addToGroup(self._graphics_item) self._label = QGraphicsSimpleTextItem(label) self._label.setFont(GraphItem._LABEL_FONT) label_rect = self._label.boundingRect() if label_pos is None: label_rect.moveCenter(bounding_box.center()) else: label_rect.moveCenter(label_pos) self._label.setPos(label_rect.x(), label_rect.y()) self.addToGroup(self._label) if tooltip is not None: self.setToolTip(tooltip) self.set_node_color() self.setAcceptHoverEvents(True) self.hovershape = None def parse_shape(self, shape, bounding_box): if shape in ('box', 'rect', 'rectangle'): self._graphics_item = QGraphicsRectItem(bounding_box) elif shape in ('ellipse', 'oval', 'circle'): self._graphics_item = QGraphicsEllipseItem(bounding_box) elif shape in ('box3d', ): self._graphics_item = QGraphicsBox3dItem(bounding_box) else: print("Invalid shape '%s', defaulting to ellipse" % shape, file=sys.stderr) self._graphics_item = QGraphicsEllipseItem(bounding_box) def set_hovershape(self, newhovershape): self.hovershape = newhovershape def shape(self): if self.hovershape is not None: path = QPainterPath() path.addRect(self.hovershape) return path else: return super(self.__class__, self).shape() def add_incoming_edge(self, edge): self._incoming_edges.add(edge) def add_outgoing_edge(self, edge): self._outgoing_edges.add(edge) def set_node_color(self, color=None): if color is None: color = self._default_color self._brush.setColor(color) self._ellipse_pen.setColor(color) self._label_pen.setColor(color) self._graphics_item.setPen(self._ellipse_pen) self._label.setBrush(self._brush) self._label.setPen(self._label_pen) def hoverEnterEvent(self, event): # hovered node item in red self.set_node_color(self._COLOR_RED) if self._highlight_level > 1: cyclic_edges = self._incoming_edges.intersection( self._outgoing_edges) # incoming edges in blue incoming_nodes = set() for incoming_edge in self._incoming_edges.difference(cyclic_edges): incoming_edge.set_node_color(self._COLOR_BLUE) incoming_edge.set_label_color(self._COLOR_BLUE) if incoming_edge.from_node != self: incoming_nodes.add(incoming_edge.from_node) # outgoing edges in green outgoing_nodes = set() for outgoing_edge in self._outgoing_edges.difference(cyclic_edges): outgoing_edge.set_node_color(self._COLOR_GREEN) outgoing_edge.set_label_color(self._COLOR_GREEN) if outgoing_edge.to_node != self: outgoing_nodes.add(outgoing_edge.to_node) # incoming/outgoing edges in teal for edge in cyclic_edges: edge.set_node_color(self._COLOR_TEAL) if self._highlight_level > 2: cyclic_nodes = incoming_nodes.intersection(outgoing_nodes) # incoming nodes in blue for incoming_node in incoming_nodes.difference(cyclic_nodes): incoming_node.set_node_color(self._COLOR_BLUE) # outgoing nodes in green for outgoing_node in outgoing_nodes.difference(cyclic_nodes): outgoing_node.set_node_color(self._COLOR_GREEN) # incoming/outgoing nodes in teal for node in cyclic_nodes: node.set_node_color(self._COLOR_TEAL) def hoverLeaveEvent(self, event): self.set_node_color() if self._highlight_level > 1: for incoming_edge in self._incoming_edges: incoming_edge.set_node_color() incoming_edge.set_label_color() if self._highlight_level > 2 and incoming_edge.from_node != self: incoming_edge.from_node.set_node_color() for outgoing_edge in self._outgoing_edges: outgoing_edge.set_node_color() outgoing_edge.set_label_color() if self._highlight_level > 2 and outgoing_edge.to_node != self: outgoing_edge.to_node.set_node_color()
class ExplorerNode(BaseNode): def __init__(self, nx_node, center_pos, nx_pos, steps, steps_max, small): """ Create node in the graph scene :param tuple nx_node: Node info :param center_pos: The position of the center node :param nx_pos: Position of the nodes in the graph :param int steps: The steps from the center identity :param int steps_max: The steps max of the graph :param bool small: Small dots for big networks """ super().__init__(nx_node, nx_pos) self.steps = steps self.steps_max = steps_max self.highlighted = False self.status_sentry = False if small: self.setRect(0, 0, 10, 10) self.text_item = None else: # text inside ellipse self.text_item = QGraphicsSimpleTextItem(self) self.text_item.setText(self.text) # center ellipse around text self.setRect(0, 0, self.text_item.boundingRect().width() * 2, self.text_item.boundingRect().height() * 2) # center text in ellipse self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) # set anchor to the center self.setTransform(QTransform().translate( -self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) # cursor change on hover self.setAcceptHoverEvents(True) self.setZValue(1) # animation and moves self.timeline = None self.loading_timer = QTimer() self.loading_timer.timeout.connect(self.next_tick) self.loading_counter = 0 self._refresh_colors() self.setPos(center_pos) self.move_to(nx_pos) def update_metadata(self, metadata): super().update_metadata(metadata) self.status_sentry = self.metadata[ 'is_sentry'] if 'is_sentry' in self.metadata else False self._refresh_colors() def _refresh_colors(self): """ Refresh elements in the node """ # color around ellipse outline_color = QColor('grey') outline_style = Qt.SolidLine outline_width = 1 if self.status_wallet: outline_width = 2 if not self.status_member: outline_color = QColor('red') if self.status_sentry: outline_color = QColor('black') outline_width = 3 self.setPen(QPen(outline_color, outline_width, outline_style)) if self.highlighted: text_color = QColor('grey') else: text_color = QColor('black') if self.status_wallet == NodeStatus.HIGHLIGHTED: text_color = QColor('grey') if self.text_item: self.text_item.setBrush(QBrush(text_color)) # create gradient inside the ellipse gradient = QRadialGradient( QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width()) color = QColor() color.setHsv(120 - 60 / self.steps_max * self.steps, 180 + 50 / self.steps_max * self.steps, 60 + 170 / self.steps_max * self.steps) if self.highlighted: color = color.darker(200) color = color.lighter( math.fabs(math.sin(self.loading_counter / 100 * math.pi) * 100) + 100) gradient.setColorAt(0, color) gradient.setColorAt(1, color.darker(150)) self.setBrush(QBrush(gradient)) def move_to(self, nx_pos): """ Move to corresponding position :param nx_pos: :return: """ origin_x = self.x() origin_y = self.y() final_x = nx_pos[self.id][0] final_y = nx_pos[self.id][1] def frame_move(frame): value = self.timeline.valueForTime(self.timeline.currentTime()) x = origin_x + (final_x - origin_x) * value y = origin_y + (final_y - origin_y) * value self.setPos(x, y) if self.scene(): self.scene().node_moved.emit(self.id, x, y) def timeline_ends(): self.setPos(final_x, final_y) self.timeline = None # Remember to hold the references to QTimeLine and QGraphicsItemAnimation instances. # They are not kept anywhere, even if you invoke QTimeLine.start(). self.timeline = QTimeLine(1000) self.timeline.setFrameRange(0, 100) self.timeline.frameChanged.connect(frame_move) self.timeline.finished.connect(timeline_ends) self.timeline.start() def highlight(self): """ Highlight the edge in the scene """ self.highlighted = True self._refresh_colors() self.update(self.boundingRect()) def neutralize(self): """ Neutralize the edge in the scene """ self.highlighted = False self._refresh_colors() self.update(self.boundingRect()) def start_loading_animation(self): """ Neutralize the edge in the scene """ if not self.loading_timer.isActive(): self.loading_timer.start(10) def stop_loading_animation(self): """ Neutralize the edge in the scene """ self.loading_timer.stop() self.loading_counter = 100 self._refresh_colors() self.update(self.boundingRect()) def next_tick(self): """ Next tick :return: """ self.loading_counter += 1 self.loading_counter %= 100 self._refresh_colors() self.update(self.boundingRect())
class Node(QGraphicsItem): logger = logging.getLogger('ViewNode') i = 0 NODE_MIN_WIDTH = 100 NODE_MAX_WIDTH = 150 NODE_HEIGHT = 50 NODE_COLOR = (152, 193, 217) # LIGHT BLUE TACTIC_COLOR = (255, 51, 51) # RED STRATEGY_COLOR = (77, 255, 77) # GREEN ROLE_COLOR = (166, 77, 255) # PURPLE KEEPER_COLOR = (255, 255, 26) # YELLOW OTHER_SUBTREE_COLOR = (147, 147, 147) # GREY DECORATOR_COLOR = (51, 51, 255) # DARK BLUE COMPOSITE_COLOR = (255, 153, 0) # ORANGE OTHER_NODE_TYPES_COLOR = (255, 102, 153) # PINK DEFAULT_SIMULATOR_COLOR = Qt.white def __init__(self, x: float, y: float, scene: QGraphicsScene, model_node: ModelNode, title: str = None, parent: QGraphicsItem = None, node_types: NodeTypes = None): """ The constructor for a UI node :param x: x position for the center of the node :param y: y position for the center of the node :param title: title of the node displayed in the ui :param parent: parent of this graphics item """ if title: self.title = title else: # give node a unique title self.title = "node {}".format(Node.i) self.id = model_node.id self.x = x self.y = y Node.i += 1 self.scene = scene self.model_node = model_node self.children = [] self.edges = [] # store node positional data when detaching from parent self.expand_data = None # add node name label centered in the eclipse, elide if title is too long self.node_text = QGraphicsSimpleTextItem() metrics = QFontMetrics(self.node_text.font()) elided_title = metrics.elidedText(self.title, Qt.ElideRight, self.NODE_MAX_WIDTH) self.node_text.setText(elided_title) self.node_text.setAcceptedMouseButtons(Qt.NoButton) self.node_text.setAcceptHoverEvents(False) self.text_width = self.node_text.boundingRect().width() self.text_height = self.node_text.boundingRect().height() self.node_text.setX(x - (self.text_width / 2)) # call super function now we know the node size super(Node, self).__init__(parent) self.node_text.setParentItem(self) # indicates if node is being dragged self.dragging = False self.setCursor(Qt.PointingHandCursor) self.setAcceptHoverEvents(True) # give the node a default color self.brush = QBrush(QColor(*self.NODE_COLOR)) self.simulator_brush = QBrush(self.DEFAULT_SIMULATOR_COLOR) # give node another color if node_types: # check for node types and color them types = node_types.get_node_type_by_name(model_node.title) if len(types) > 0: category, node_type = types[0] if category == 'decorators': self.brush.setColor(QColor(*self.DECORATOR_COLOR)) elif category == 'composites': self.brush.setColor(QColor(*self.COMPOSITE_COLOR)) else: self.brush.setColor(QColor(*self.OTHER_NODE_TYPES_COLOR)) # check for a strategy, role, tactic or keeper if 'name' in model_node.attributes.keys() or 'role' in model_node.attributes.keys(): if model_node.title == 'Tactic': self.brush.setColor(QColor(*self.TACTIC_COLOR)) elif model_node.title == 'Strategy': self.brush.setColor(QColor(*self.STRATEGY_COLOR)) elif model_node.title == 'Keeper': self.brush.setColor(QColor(*self.KEEPER_COLOR)) elif model_node.title == 'Role': self.brush.setColor(QColor(*self.ROLE_COLOR)) else: self.brush.setColor(QColor(*self.OTHER_SUBTREE_COLOR)) self.info_display = [] self.max_width = 0 self.total_height = 0 self.bottom_collapse_expand_button = None self.top_collapse_expand_button = None self._rect = None self.initiate_view() def initiate_view(self, propagate=False): """ Initiates all the children for the current view :param propagate: Propagate initiate view signal to children """ for rect in self.info_display: rect.setParentItem(None) if self.top_collapse_expand_button and self.bottom_collapse_expand_button: self.top_collapse_expand_button.setParentItem(None) self.bottom_collapse_expand_button.setParentItem(None) self.info_display = [] self.max_width = self.text_width + 10 self.total_height = self.NODE_HEIGHT if self.scene.info_mode: model_node = self.scene.gui.tree.nodes[self.id] self.create_info_display(self.x, self.y, model_node.attributes) if self.max_width > self.NODE_MIN_WIDTH - 10: self._rect = QRect(self.x - self.max_width / 2, self.y - self.total_height / 2, self.max_width, self.total_height) else: self._rect = QRect(self.x - self.NODE_MIN_WIDTH / 2, self.y - self.total_height / 2, self.NODE_MIN_WIDTH, self.total_height) # set node size based on children self.node_text.setY(self.y - self.total_height / 2 + self.NODE_HEIGHT / 2 - self.text_height / 2) self.create_expand_collapse_buttons() self.scene.update() if propagate: for c in self.children: c.initiate_view(True) for e in self.edges: e.change_position() def create_expand_collapse_buttons(self): """ Creates the expand/collapse buttons of the node """ # create the bottom collapse/expand button for this node if self.bottom_collapse_expand_button: bottom_collapsed = self.bottom_collapse_expand_button.isCollapsed else: bottom_collapsed = False self.bottom_collapse_expand_button = CollapseExpandButton(self) self.bottom_collapse_expand_button.setParentItem(self) self.bottom_collapse_expand_button.collapse.connect(self.collapse_children) self.bottom_collapse_expand_button.expand.connect(self.expand_children) self.bottom_collapse_expand_button.isCollapsed = bottom_collapsed # position the bottom button at the bottom-center of the node button_x = self.x - (self.bottom_collapse_expand_button.boundingRect().width() / 2) button_y = self.y + self.total_height / 2 - (self.bottom_collapse_expand_button.boundingRect().height() / 2) self.bottom_collapse_expand_button.setPos(button_x, button_y) # hidden by default, the button is only needed if the node has children if not self.children: self.bottom_collapse_expand_button.hide() # create the top collapse/expand button for this node if self.top_collapse_expand_button: top_collapsed = self.top_collapse_expand_button.isCollapsed else: top_collapsed = False self.top_collapse_expand_button = CollapseExpandButton(self) self.top_collapse_expand_button.setParentItem(self) self.top_collapse_expand_button.collapse.connect(self.collapse_upwards) self.top_collapse_expand_button.expand.connect(self.expand_upwards) self.top_collapse_expand_button.isCollapsed = top_collapsed if self.scene.root_ui_node == self or self in self.scene.disconnected_nodes \ or self.scene.reconnecting_node == self: self.top_collapse_expand_button.hide() # position the top button at the top-center of the node button_x = self.x - (self.top_collapse_expand_button.boundingRect().width() / 2) button_y = self.y - self.total_height / 2 - (self.top_collapse_expand_button.boundingRect().height() / 2) self.top_collapse_expand_button.setPos(button_x, button_y) def create_info_display(self, x, y, attributes): """ Creates view elements for the info display :param x: x position of the node :param y: y position of the node :param attributes: attributes that will be displayed in the view :return: """ start_height = y + (self.NODE_HEIGHT / 2) # unfold dictionary values at the bottom of the list sorted_attributes = [] for k, v in sorted(attributes.items(), key=lambda tup: isinstance(tup[1], dict)): if isinstance(v, dict): sorted_attributes.append((k, v)) sorted_attributes.extend(v.items()) else: sorted_attributes.append((k, v)) # create property rows for i, (k, v) in enumerate(sorted_attributes): value_text = None value_height = 0 if isinstance(v, dict): # display dictionary key as title text = "{}".format(k) if len(text) > 20: text = text[:20] + "..." key_text = QGraphicsSimpleTextItem(text) f = key_text.font() f.setBold(True) key_text.setFont(f) text_width = key_text.boundingRect().width() else: key_text = QGraphicsSimpleTextItem("{}:".format(k) if k else " ") text = str(v) if len(text) > 20: text = text[:20] + "..." value_text = QGraphicsSimpleTextItem(text) value_height = value_text.boundingRect().height() text_width = key_text.boundingRect().width() + value_text.boundingRect().width() # create box around property attribute_container = QGraphicsRectItem(x, start_height, text_width + 10, max(key_text.boundingRect().height(), value_height) + 10) attribute_container.setBrush(QBrush(Qt.white)) self.total_height += attribute_container.rect().height() key_text.setParentItem(attribute_container) if value_text: value_text.setParentItem(attribute_container) self.max_width = max(self.max_width, attribute_container.rect().width()) attribute_container.setParentItem(self) self.info_display.append(attribute_container) start_height += max(key_text.boundingRect().height(), value_height) + 10 # calculate correct coordinates for positioning of the attribute boxes if self.max_width > self.NODE_MIN_WIDTH - 10: x -= (self.max_width + 10) / 2 y -= self.total_height / 2 self.max_width += 10 else: x -= self.NODE_MIN_WIDTH / 2 y -= self.total_height / 2 self.max_width = self.NODE_MIN_WIDTH h = 0 # position all the elements previously created for attribute_container in self.info_display: rect: QRectF = attribute_container.rect() rect.setX(x) rect_height = rect.height() rect.setY(y + self.NODE_HEIGHT + h) rect.setHeight(rect_height) key_child = attribute_container.childItems()[0] if len(attribute_container.childItems()) == 2: key_child.setX(x + 5) value_child = attribute_container.childItems()[1] value_child.setX(x + self.max_width - value_child.boundingRect().width() - 5) value_child.setY(y + self.NODE_HEIGHT + h + 5) else: key_child.setX(x - key_child.boundingRect().width() / 2 + self.max_width / 2) key_child.setY(y + self.NODE_HEIGHT + h + 5) h += rect.height() rect.setWidth(self.max_width) attribute_container.setRect(rect) def paint(self, painter: QPainter, style_options: QStyleOptionGraphicsItem, widget=None): """ Paint the basic shape of the node (ellipse or rectangle) :param painter: painter used to paint objects :param style_options: Styling options for the graphics item :param widget: The widget being painted """ painter.setPen(Qt.SolidLine) if self == self.scene.root_ui_node: pen = QPen(Qt.black, 2.0) pen.setStyle(Qt.DotLine) painter.setPen(pen) if self.scene.simulator_mode: brush = self.simulator_brush else: brush = self.brush painter.setBrush(brush) if self.scene.info_mode: painter.drawRect(self.rect().x(), self.rect().y(), self.rect().width(), self.NODE_HEIGHT) else: painter.drawEllipse(self.rect()) def add_child(self, child): """ Add a child node Inheritance looks like: parent > edge > child :param child: Another ui node """ edge = Edge(self, child) edge.setParentItem(self) # edge should stay behind the expand/collapse button edge.stackBefore(self.bottom_collapse_expand_button) self.children.append(child) self.edges.append(edge) # show the expand/collapse button when the first child is added if not self.bottom_collapse_expand_button.isVisible(): self.bottom_collapse_expand_button.show() if not child.top_collapse_expand_button.isVisible(): child.top_collapse_expand_button.show() def remove_child(self, child): """ Removes child from this node (no data changes) :param child: Child of this node """ if child not in self.children: Node.logger.error("Incorrect child can not be removed from wrong parent.") edge = child.parentItem() child.setParentItem(None) self.children.remove(child) self.edges.remove(edge) edge.setParentItem(None) self.scene.removeItem(edge) if not self.children: self.bottom_collapse_expand_button.hide() def nodes_below(self): nodes = [] for c in self.children: nodes.append(c) nodes.extend(c.nodes_below()) return nodes def moveBy(self, x, y): super(Node, self).moveBy(x, y) # move edge correctly with node if self.parentItem() and isinstance(self.parentItem(), Edge): self.parentItem().change_position() def setPos(self, *args): super(Node, self).setPos(*args) # move edge correctly with node if self.parentItem() and isinstance(self.parentItem(), Edge): self.parentItem().change_position() def xoffset(self): """ recursively adds the relative x distances from this node up until the root node. :return: the sum of the relative x distances """ if self.parentItem(): return self.pos().x() + self.parentItem().xoffset() else: return self.pos().x() + self.rect().x() + self.rect().width() / 2 def yoffset(self): """ recursively adds the relative y distances from this node up until the root node. :return: the sum of the relative y distances """ if self.parentItem(): return self.pos().y() + self.parentItem().yoffset() else: return self.pos().y() + self.rect().y() + self.rect().height() / 2 def xpos(self): """ Calculates the x position of this node using the x offset :return: the x position of the node """ return self.xoffset() def ypos(self): """ Calculates the y position of this node using the y offset :return: the y position of the node """ return self.yoffset() def boundingRect(self): return QRectF(self._rect) def rect(self): return self._rect def detach_from_parent(self): """ Detaches node from parent (no data changes) :return: Positional data that can be used to reattach node """ if not self.parentItem() or not self.parentItem().parentItem(): Node.logger.error("The node can't detach from parent, no parent") return # store attach data used to restore the state when attaching xpos, ypos = self.xpos(), self.ypos() root_item = self.scene.root_ui_node parent_node = self.parentItem().parentItem() attach_data = { "abs_pos": QPointF(xpos, ypos), "old_parent": parent_node, "top_level_item": self.topLevelItem(), } parent_node.remove_child(self) # move node to retain correct position self.setPos(0, 0) root_x = root_item.xpos() if root_item else self.scene.node_init_pos[0] root_y = root_item.ypos() if root_item else self.scene.node_init_pos[1] move_x = xpos - root_x - (self.scene.node_init_pos[0] - root_x) move_y = ypos - root_y - (self.scene.node_init_pos[1] - root_y) self.moveBy(move_x, move_y) return attach_data def attach_to_parent(self, data, parent=None): """ Attaches node to parent (no data changes) :param: data: Positional data from detachment used for attaching """ if not parent: parent = data['old_parent'] new_abs_pos = QPointF(self.xpos(), self.ypos()) # reset parent item e = Edge(parent, self) e.setParentItem(parent) parent.children.append(self) parent.edges.append(e) parent.sort_children() parent_abs_pos = QPointF(parent.xpos(), parent.ypos()) # reset relative position to parent self.setPos(new_abs_pos - parent_abs_pos) def collapse_upwards(self): """ Collapses the tree upwards only displaying this node and its children :return: """ self.expand_data = self.detach_from_parent() # hide parent nodes self.expand_data['top_level_item'].hide() def expand_upwards(self): """ Expands the tree upwards displaying all expanded parent nodes :return: """ self.attach_to_parent(self.expand_data) # show expanded parent nodes self.topLevelItem().show() def collapse_children(self): """ Collapses this node's children by hiding all child edges (and therefore the whole subtree) """ for c in self.childItems(): if isinstance(c, Edge): c.hide() def expand_children(self): """ Expands this node's children by showing all child edges previously hidden by the collapse function """ for c in self.childItems(): if isinstance(c, Edge): c.show() def sort_children(self): """ Sort child edges/nodes based on x position :return: The model nodes in order """ # gather all the edges child_edges = [edge for edge in self.childItems() if isinstance(edge, Edge)] # sort edges by x position of the child nodes child_edges.sort(key=lambda c: c.end_node.xpos()) # reset internal structure self.edges.clear() self.children.clear() # add children back in correct order for e in child_edges: e.setParentItem(None) self.edges.append(e) self.children.append(e.end_node) # set the parent of the children in the correct order for e in child_edges: e.setParentItem(self) # return the model nodes in the correct order model_nodes_order = [e.end_node.model_node for e in child_edges] return model_nodes_order def detect_order_change(self): """ Detects if node order has changed and updates model accordingly """ if not self.parentItem(): # sort top level nodes, this prevents alignment issues self.scene.disconnected_nodes = sorted(self.scene.disconnected_nodes, key=lambda n: n.xpos()) else: # parent node of self parent_node = self.parentItem().parentItem() parent_model_node = self.scene.gui.tree.nodes.get(parent_node.id) # own child index node_index = parent_node.children.index(self) # check if node is swapped with left neighbour try: if node_index - 1 >= 0: # can throw IndexError if there is no left neighbour left_node = parent_node.children[node_index - 1] # check if node is swapped if left_node.xpos() > self.xpos(): # sort children of parent sorted_nodes = parent_node.sort_children() # change model tree structure accordingly parent_model_node.children = [n.id for n in sorted_nodes] self.scene.gui.update_tree(parent_model_node) except IndexError: pass # check if node is swapped with right neighbour try: # can throw IndexError if there is no right neighbour right_node = parent_node.children[node_index + 1] # check if node is swapped if right_node.xpos() < self.xpos(): # sort children of parent sorted_nodes = parent_node.sort_children() # change model tree structure accordingly parent_model_node.children = [n.id for n in sorted_nodes] self.scene.gui.update_tree(parent_model_node) except IndexError: pass def delete_self(self): """ Deletes this node and makes children disconnected subtrees/nodes """ for c in self.children[:]: c.detach_from_parent() # add child to disconnected nodes if self in self.scene.disconnected_nodes: index = self.scene.disconnected_nodes.index(self) self.scene.disconnected_nodes.insert(index, c) else: self.scene.disconnected_nodes.insert(0, c) c.top_collapse_expand_button.hide() parent_model_node = None if self.parentItem(): parent_node: Node = self.parentItem().parentItem() parent_node.remove_child(self) parent_model_node = self.scene.gui.tree.nodes.get(parent_node.id) parent_model_node.children.remove(self.id) if self in self.scene.disconnected_nodes: self.scene.disconnected_nodes.remove(self) self.scene.removeItem(self) self.scene.close_property_display() del self.scene.nodes[self.id] # reset root if this is the root if self.scene.gui.tree.root == self.id: self.scene.gui.tree.root = '' # remove node from internal tree structure del self.scene.gui.tree.nodes[self.id] if parent_model_node: self.scene.gui.update_tree(parent_model_node) def delete_subtree(self, delete_parent_relation=True, update_tree=True): """ Deletes node and its children :param delete_parent_relation: Boolean indicating if parent relation should be modified :param update_tree: Boolean indicating if the tree needs an update """ # remove children for c in self.children: c.delete_subtree(delete_parent_relation=False) # remove child reference from parent parent_node = None if delete_parent_relation and self.parentItem(): parent_node: Node = self.parentItem().parentItem() parent_node.remove_child(self) try: self.scene.gui.tree.nodes[parent_node.id].children.remove(self.id) except ValueError: pass self.scene.removeItem(self) self.scene.close_property_display() if self in self.scene.disconnected_nodes: self.scene.disconnected_nodes.remove(self) self.scene.nodes.pop(self.id, None) if self.scene.gui.tree.root == self.id: self.scene.gui.tree.root = '' # remove node from internal tree structure self.scene.gui.tree.nodes.pop(self.id, None) if delete_parent_relation and parent_node and update_tree: node = self.scene.gui.tree.nodes.get(parent_node.id) self.scene.gui.update_tree(node) def reconnect_edge(self): """ Starts edge reconnection process """ if not self.parentItem() and self not in self.scene.disconnected_nodes: Node.logger.error("The edge trying to reconnect does not exist.") else: self.scene.start_reconnect_edge(self) def mousePressEvent(self, m_event): """ Handles a mouse press on a node :param m_event: The mouse press event and its details """ super(Node, self).mousePressEvent(m_event) tree = self.scene.gui.tree.nodes[self.id] if self.scene.view.parent().property_display: self.scene.view.parent().property_display.setParent(None) self.scene.view.parent().property_display.deleteLater() self.scene.view.parent().property_display = view.widgets.TreeViewPropertyDisplay( self.scene.view.parent().graphics_scene, tree.attributes, parent=self.scene.view.parent(), node_id=tree.id, node_title=tree.title) def mouseMoveEvent(self, m_event): """ Handles a mouse move over a node :param m_event: The mouse move event and its details """ super(Node, self).mouseMoveEvent(m_event) if self.dragging: # move the node with the mouse and adjust the edges to the new position dx = m_event.scenePos().x() - m_event.lastScenePos().x() dy = m_event.scenePos().y() - m_event.lastScenePos().y() self.setPos(self.pos().x() + dx, self.pos().y() + dy) # Set correct order for children if node has a parent and the order of disconnected nodes self.detect_order_change() # reposition incoming edge if isinstance(self.parentItem(), Edge): self.parentItem().change_position() def contextMenuEvent(self, menu_event): """ Creates context menu for right clicks on this node :param menu_event: Context about the right click event """ menu = QMenu() reconnect_edge_action = QAction("Reconnect Edge" if self.parentItem() else "Connect Edge") reconnect_edge_action.triggered.connect(self.reconnect_edge) menu.addAction(reconnect_edge_action) delete_action = QAction("Delete Node") delete_action.setToolTip('Delete only this node.') delete_action.triggered.connect(self.delete_self) menu.addAction(delete_action) delete_subtree_action = QAction("Delete Subtree") delete_subtree_action.setToolTip('Delete node and all its children.') delete_subtree_action.triggered.connect(lambda: self.delete_subtree()) menu.addAction(delete_subtree_action) menu.exec(menu_event.screenPos()) menu_event.setAccepted(True)
class IndicatorIconView(QGraphicsPixmapItem): def __init__(self, parent: ViewBox, icon_path: str, icon_pos: int, color: Optional[List[int]] = None, message: str = ""): """An indicator icon for a pyqtgraph ViewBox The icon loaded from icon_path will be displayed in the low right corner of the ViewBox. :param parent: ViewBox to place indicator in :param icon_path: path to icon :param icon_pos: position index. Counting from 0 in lower right. """ super().__init__() self.parent = parent self.icon_pos = icon_pos self.label = QGraphicsSimpleTextItem(message) self.label.setVisible(False) self.parent.scene().addItem(self.label) self.set_icon(icon_path, color) self.icon_size = [32, 32] self.parent.scene().addItem(self) self.position_icon() self.parent.sigResized.connect(self.position_icon) self.setVisible(False) self.setAcceptHoverEvents(True) self.connected_overlay = None self.actions: List[QAction] = [] def set_icon(self, icon_path: str, color: Optional[List[int]] = None): if color is not None: image_data = skio.imread(icon_path, plugin="imageio") # Set the RGB part to the red channel multiplied by the requested color red_channel = image_data[:, :, 0] / 255 image_data[:, :, 0] = red_channel * color[0] image_data[:, :, 1] = red_channel * color[1] image_data[:, :, 2] = red_channel * color[2] h = image_data.shape[0] w = image_data.shape[1] image_qi = QImage(image_data.data, w, h, 4 * w, QImage.Format_RGBA8888) image_pm = QPixmap.fromImage(image_qi) self.label.setBrush(QColor(*color)) else: image_pm = QPixmap(icon_path) self.setPixmap(image_pm) def position_icon(self): # The size of the imageview we are putting the icon ing scene_size = self.parent.size() # The position of the image within the scene scene_pos = self.parent.scenePos() # Lower right corner in scene pixel coordinates corner_pos_x = scene_size.width() + scene_pos.x() corner_pos_y = scene_size.height() + scene_pos.y() icon_pos_x = corner_pos_x - self.icon_size[0] * (1 + self.icon_pos) - 10 icon_pos_y = corner_pos_y - self.icon_size[1] - 30 self.setOffset(icon_pos_x, icon_pos_y) label_width = self.label.boundingRect().width() self.label.setPos(corner_pos_x - label_width, icon_pos_y - self.icon_size[0]) def hoverEnterEvent(self, event): if self.connected_overlay is not None: self.connected_overlay.setVisible(True) self.label.setVisible(True) def hoverLeaveEvent(self, event): if self.connected_overlay is not None: self.connected_overlay.setVisible(False) self.label.setVisible(False) def add_actions(self, actions: List[Tuple[str, Callable]]): for text, method in actions: action = QAction(text) action.triggered.connect(method) self.actions.append(action) def mouseClickEvent(self, event): event.accept() if self.actions: qm = QMenu() for action in self.actions: qm.addAction(action) qm.exec(event.screenPos().toQPoint())
class VLineChartView(QChartView): bar_hovered = pyqtSignal(bool, str) def __init__(self, data: pd.DataFrame): super(VLineChartView, self).__init__() self._stocks = data self._category = self._stocks['trade_date'] self._chart = QChart() self._chart.setAnimationOptions(QChart.SeriesAnimations) self._series = QStackedBarSeries() # 成交量以万股为单位 self._vol_multiple = 10000 self.init_chart() self._zero_value = (0, self._chart.axisY().min()) self._max_value = (len(self._chart.axisX().categories()), self._chart.axisY().max()) self._zero_point = self._chart.mapToPosition( QPointF(self._zero_value[0], self._zero_value[1])) self._max_point = self._chart.mapToPosition( QPointF(self._max_value[0], self._max_value[1])) # 计算x轴单个cate的宽度,用来处理横线不能画到边界 self._cate_width = (self._max_point.x() - self._zero_point.x()) / len( self._category) self._series.hovered.connect(self.on_series_hovered) x_index_list = np.percentile(range(len(self._category)), [0, 25, 50, 75, 100]) self._x_axis_list = [ QGraphicsSimpleTextItem(self._category[int(index)], self._chart) for index in x_index_list ] [axis.setText(axis.text()[4:]) for axis in self._x_axis_list[1:]] self._v_b = QGraphicsSimpleTextItem('B', self._chart) self._v_b.setZValue(100) def on_series_hovered(self, status, index): self.bar_hovered.emit(status, self._category[index]) def clear_series_value(self): self._series.clear() self._stocks = None self._chart.axisY().setRange(0, 10) self._chart.axisX().setCategories(list()) def add_series_values(self, data: pd.DataFrame, is_init=False): self._stocks = data bar_red = QBarSet('red') bar_red.setColor(Qt.red) bar_green = QBarSet('green') bar_green.setColor(Qt.green) for _, stock in self._stocks.iterrows(): if stock['open'] < stock['close']: bar_red.append(stock['vol'] / self._vol_multiple) bar_green.append(0) else: bar_red.append(0) bar_green.append(stock['vol'] / self._vol_multiple) self._series.append(bar_red) self._series.append(bar_green) if not is_init: self._stocks = data self._category = self._stocks['trade_date'] axis_x = self._chart.axisX() axis_y = self._chart.axisY() axis_x.setCategories(self._category) max_p = self._stocks[[ 'vol', ]].stack().max() min_p = self._stocks[[ 'vol', ]].stack().min() axis_y.setRange(min_p / self._vol_multiple * 0.9, max_p / self._vol_multiple * 1.1) self._zero_value = (0, self._chart.axisY().min()) self._max_value = (len(self._chart.axisX().categories()), self._chart.axisY().max()) # 计算x轴单个cate的宽度,用来处理横线不能画到边界 self._cate_width = (self._max_point.x() - self._zero_point.x()) / len(self._category) def resizeEvent(self, event): super(VLineChartView, self).resizeEvent(event) self._zero_point = self._chart.mapToPosition( QPointF(self._zero_value[0], self._zero_value[1])) self._max_point = self._chart.mapToPosition( QPointF(self._max_value[0], self._max_value[1])) self._cate_width = (self._max_point.x() - self._zero_point.x()) / len( self._category) # 绘制自定义X轴 self._x_axis_list[0].setPos(self._zero_point.x() - self._cate_width, self._zero_point.y() + 10) self._x_axis_list[1].setPos(self._max_point.x() * 0.25, self._zero_point.y() + 10) self._x_axis_list[2].setPos(self._max_point.x() * 0.5, self._zero_point.y() + 10) self._x_axis_list[3].setPos(self._max_point.x() * 0.75, self._zero_point.y() + 10) self._x_axis_list[4].setPos( self._max_point.x() - self._x_axis_list[-1].boundingRect().width(), self._zero_point.y() + 10) # 20180207 这个日期的柱形图上面画一个字母b vol = self._stocks[self._stocks['trade_date'] == '20180207']['vol'] / self._vol_multiple print('vol:', vol, ' trade_date:', '20180207') pos = self._chart.mapToPosition( QPointF(list(self._category).index('20180207'), vol)) pos = QPointF(pos.x() - self._cate_width / 2, pos.y() - self._v_b.boundingRect().height()) self._v_b.setPos(pos) def max_point(self): return QPointF(self._max_point.x() + self._cate_width / 2, self._max_point.y()) def min_point(self): return QPointF(self._zero_point.x() - self._cate_width / 2, self._zero_point.y()) def init_chart(self): self.add_series_values(self._stocks, True) self._chart.addSeries(self._series) self._chart.createDefaultAxes() self._chart.setLocalizeNumbers(True) axis_x = self._chart.axisX() axis_y = self._chart.axisY() axis_x.setGridLineVisible(False) axis_y.setGridLineVisible(False) axis_y.setLabelFormat("%.2f") axis_x.setCategories(self._category) axis_x.setLabelsVisible(False) max_p = self._stocks[[ 'vol', ]].stack().max() min_p = self._stocks[[ 'vol', ]].stack().min() axis_y.setRange(min_p / self._vol_multiple * 0.9, max_p / self._vol_multiple * 1.1) # chart的图例 legend = self._chart.legend() legend.hide() # 设置图例由Series来决定样式 # legend.setMarkerShape(QLegend.MarkerShapeFromSeries) self.setChart(self._chart) self._chart.layout().setContentsMargins(0, 0, 0, 0) # 设置内边界的bottom为0 # margins = self._chart.margins() # self._chart.setMargins(QMargins(margins.left(), 0, margins.right(), 0)) self._chart.setBackgroundRoundness(0)
class ExplorerNode(BaseNode): def __init__(self, nx_node, center_pos, nx_pos, steps, steps_max): """ Create node in the graph scene :param tuple nx_node: Node info :param center_pos: The position of the center node :param nx_pos: Position of the nodes in the graph :param int steps: The steps from the center identity :param int steps_max: The steps max of the graph """ super().__init__(nx_node, nx_pos) self.steps = steps self.steps_max = steps_max self.highlighted = False # text inside ellipse self.text_item = QGraphicsSimpleTextItem(self) self.text_item.setText(self.text) # center ellipse around text self.setRect( 0, 0, self.text_item.boundingRect().width() * 2, self.text_item.boundingRect().height() * 2 ) # set anchor to the center self.setTransform( QTransform().translate(-self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) # center text in ellipse self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) # cursor change on hover self.setAcceptHoverEvents(True) self.setZValue(1) # animation and moves self.timeline = None self.loading_timer = QTimer() self.loading_timer.timeout.connect(self.next_tick) self.loading_counter = 0 self._refresh_colors() self.setPos(center_pos) self.move_to(nx_pos) def _refresh_colors(self): """ Refresh elements in the node """ # color around ellipse outline_color = QColor('black') outline_style = Qt.SolidLine outline_width = 1 if self.status_wallet: outline_color = QColor('grey') outline_width = 2 if not self.status_member: outline_color = QColor('red') outline_style = Qt.SolidLine self.setPen(QPen(outline_color, outline_width, outline_style)) if self.highlighted: text_color = QColor('grey') else: text_color = QColor('black') if self.status_wallet == NodeStatus.HIGHLIGHTED: text_color = QColor('grey') self.text_item.setBrush(QBrush(text_color)) # create gradient inside the ellipse gradient = QRadialGradient(QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width()) color = QColor() color.setHsv(120 - 60 / self.steps_max * self.steps, 180 + 50 / self.steps_max * self.steps, 60 + 170 / self.steps_max * self.steps) if self.highlighted: color = color.darker(200) color = color.lighter(math.fabs(math.sin(self.loading_counter / 100 * math.pi) * 100) + 100) gradient.setColorAt(0, color) gradient.setColorAt(1, color.darker(150)) self.setBrush(QBrush(gradient)) def move_to(self, nx_pos): """ Move to corresponding position :param nx_pos: :return: """ origin_x = self.x() origin_y = self.y() final_x = nx_pos[self.id][0] final_y = nx_pos[self.id][1] def frame_move(frame): value = self.timeline.valueForTime(self.timeline.currentTime()) x = origin_x + (final_x - origin_x) * value y = origin_y + (final_y - origin_y) * value self.setPos(x, y) self.scene().node_moved.emit(self.id, x, y) def timeline_ends(): self.setPos(final_x, final_y) self.timeline = None # Remember to hold the references to QTimeLine and QGraphicsItemAnimation instances. # They are not kept anywhere, even if you invoke QTimeLine.start(). self.timeline = QTimeLine(1000) self.timeline.setFrameRange(0, 100) self.timeline.frameChanged.connect(frame_move) self.timeline.finished.connect(timeline_ends) self.timeline.start() def highlight(self): """ Highlight the edge in the scene """ self.highlighted = True self._refresh_colors() self.update(self.boundingRect()) def neutralize(self): """ Neutralize the edge in the scene """ self.highlighted = False self._refresh_colors() self.update(self.boundingRect()) def start_loading_animation(self): """ Neutralize the edge in the scene """ if not self.loading_timer.isActive(): self.loading_timer.start(10) def stop_loading_animation(self): """ Neutralize the edge in the scene """ self.loading_timer.stop() self.loading_counter = 100 self._refresh_colors() self.update(self.boundingRect()) def next_tick(self): """ Next tick :return: """ self.loading_counter += 1 self.loading_counter %= 100 self._refresh_colors() self.update(self.boundingRect())
class SocketRow(QGraphicsWidget): def __init__(self, qt_node, pin): super(SocketRow, self).__init__() assert qt_node is not None self.setParentItem(qt_node) self._parent_node = weakref.ref(qt_node) self._pin = pin self._spacerConstant = 5.0 self._label = QGraphicsSimpleTextItem(self) self._socket = None self._outputHook = None socket_colour = QColor(*pin.colour) socket_type = pin.shape if pin.io_type == "input": self._socket = QtSocket(self, "input", socket_type) self._socket.setColor(socket_colour) else: self._socket = QtSocket(self, "output", socket_type) self._socket.setColor(socket_colour) self.setLabelColor(self.defaultColor()) self.setLabelText(self._pin.name) self._socket.setVisible(True) def parentNode(self): return self._parent_node() def pin(self): return self._pin def socket(self): return self._socket def defaultColor(self): return self._parent_node().labelColor() def labelColor(self): return self._label.brush().color() def setLabelColor(self, color): self._label.setBrush(color) def labelText(self): return self._label.text() def setLabelText(self, text): self._label.setText(text) def refresh(self): # Update cosmetics colour = QColor(*self._pin.colour) self._socket.setColor(colour) self._socket.setShape(self._pin.shape) self._socket.update() def updateLayout(self): height = self._label.boundingRect().height() hook = self._socket if hook.mode() == "output": hook_y_pos = (height - hook.boundingRect().height()) / 2.0 else: hook_y_pos = (height - hook.boundingRect().height()) / 2.0 hook.setPos(0.0, hook_y_pos) input_width = self._spacerConstant * 2.0 self._label.setPos(input_width + self._spacerConstant, 0) if hook.mode() == "output": hook.setPos(self._label.pos().x() + self._label.boundingRect().width() + self._spacerConstant, hook_y_pos) self.resize(hook.pos().x() + hook.boundingRect().width(), height) else: self.resize(self._label.pos().x() + self._label.boundingRect().width(), height) def onDeleted(self): if self._socket: self._socket.onDeleted()
class Node(QGraphicsEllipseItem): def __init__(self, metadata, x_y): """ Create node in the graph scene :param dict metadata: Node metadata :param x_y: Position of the node """ # unpack tuple x, y = x_y super(Node, self).__init__() self.metadata = metadata self.id = metadata['id'] self.status_wallet = self.metadata['status'] & NODE_STATUS_HIGHLIGHTED self.status_member = not self.metadata['status'] & NODE_STATUS_OUT self.text = self.metadata['text'] self.setToolTip(self.metadata['tooltip']) self.arcs = [] self.menu = None self.action_sign = None self.action_transaction = None self.action_contact = None self.action_show_member = None # color around ellipse outline_color = QColor('grey') outline_style = Qt.SolidLine outline_width = 1 if self.status_wallet: outline_color = QColor('black') outline_width = 2 if not self.status_member: outline_color = QColor('red') outline_style = Qt.SolidLine self.setPen(QPen(outline_color, outline_width, outline_style)) # text inside ellipse self.text_item = QGraphicsSimpleTextItem(self) self.text_item.setText(self.text) text_color = QColor('grey') if self.status_wallet == NODE_STATUS_HIGHLIGHTED: text_color = QColor('black') self.text_item.setBrush(QBrush(text_color)) # center ellipse around text self.setRect( 0, 0, self.text_item.boundingRect().width() * 2, self.text_item.boundingRect().height() * 2 ) # set anchor to the center self.setTransform( QTransform().translate(-self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) self.setPos(x, y) # center text in ellipse self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) # create gradient inside the ellipse gradient = QRadialGradient(QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width()) gradient.setColorAt(0, QColor('white')) gradient.setColorAt(1, QColor('darkgrey')) self.setBrush(QBrush(gradient)) # cursor change on hover self.setAcceptHoverEvents(True) self.setZValue(1) def mousePressEvent(self, event: QMouseEvent): """ Click on mouse button :param event: mouse event """ if event.button() == Qt.LeftButton: # trigger scene signal self.scene().node_clicked.emit(self.metadata) def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): """ Mouse enter on node zone :param event: scene hover event """ self.setCursor(Qt.ArrowCursor) def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent): """ Right click on node to show node menu Except on wallet node :param event: scene context menu event """ # no menu on the wallet node if self.status_wallet: return None # create node context menus self.menu = QMenu() # action show member QT_TRANSLATE_NOOP('WoT.Node', 'Informations') self.action_show_member = QAction(QCoreApplication.translate('WoT.Node', 'Informations'), self.scene()) self.menu.addAction(self.action_show_member) self.action_show_member.triggered.connect(self.member_action) # action add identity as contact QT_TRANSLATE_NOOP('WoT.Node', 'Add as contact') self.action_contact = QAction(QCoreApplication.translate('WoT.Node', 'Add as contact'), self.scene()) self.menu.addAction(self.action_contact) self.action_contact.triggered.connect(self.contact_action) # action transaction toward identity QT_TRANSLATE_NOOP('WoT.Node', 'Send money') self.action_transaction = QAction(QCoreApplication.translate('WoT.Node', 'Send money'), self.scene()) self.menu.addAction(self.action_transaction) self.action_transaction.triggered.connect(self.transaction_action) # action sign identity QT_TRANSLATE_NOOP('WoT.Node', 'Certify identity') self.action_sign = QAction(QCoreApplication.translate('WoT.Node', 'Certify identity'), self.scene()) self.menu.addAction(self.action_sign) self.action_sign.triggered.connect(self.sign_action) # run menu self.menu.exec(event.screenPos()) def add_arc(self, arc): """ Add arc to the arc list :param arc: Arc """ self.arcs.append(arc) def member_action(self): """ Transaction action to identity node """ # trigger scene signal self.scene().node_member.emit(self.metadata) def contact_action(self): """ Transaction action to identity node """ # trigger scene signal self.scene().node_contact.emit(self.metadata) def sign_action(self): """ Sign identity node """ # trigger scene signal self.scene().node_signed.emit(self.metadata) def transaction_action(self): """ Transaction action to identity node """ # trigger scene signal self.scene().node_transaction.emit(self.metadata)
class Node(QGraphicsEllipseItem): def __init__(self, metadata, x_y): """ Create node in the graph scene :param dict metadata: Node metadata :param x_y: Position of the node """ # unpack tuple x, y = x_y super(Node, self).__init__() self.metadata = metadata self.id = metadata['id'] self.status_wallet = self.metadata['status'] & NODE_STATUS_HIGHLIGHTED self.status_member = not self.metadata['status'] & NODE_STATUS_OUT self.text = self.metadata['text'] self.setToolTip(self.metadata['tooltip']) self.arcs = [] self.menu = None self.action_sign = None self.action_transaction = None self.action_contact = None self.action_show_member = None # color around ellipse outline_color = QColor('grey') outline_style = Qt.SolidLine outline_width = 1 if self.status_wallet: outline_color = QColor('black') outline_width = 2 if not self.status_member: outline_color = QColor('red') outline_style = Qt.SolidLine self.setPen(QPen(outline_color, outline_width, outline_style)) # text inside ellipse self.text_item = QGraphicsSimpleTextItem(self) self.text_item.setText(self.text) text_color = QColor('grey') if self.status_wallet == NODE_STATUS_HIGHLIGHTED: text_color = QColor('black') self.text_item.setBrush(QBrush(text_color)) # center ellipse around text self.setRect(0, 0, self.text_item.boundingRect().width() * 2, self.text_item.boundingRect().height() * 2) # set anchor to the center self.setTransform(QTransform().translate( -self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) self.setPos(x, y) # center text in ellipse self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) # create gradient inside the ellipse gradient = QRadialGradient( QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width()) gradient.setColorAt(0, QColor('white')) gradient.setColorAt(1, QColor('darkgrey')) self.setBrush(QBrush(gradient)) # cursor change on hover self.setAcceptHoverEvents(True) self.setZValue(1) def mousePressEvent(self, event: QMouseEvent): """ Click on mouse button :param event: mouse event """ if event.button() == Qt.LeftButton: # trigger scene signal self.scene().node_clicked.emit(self.metadata) def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): """ Mouse enter on node zone :param event: scene hover event """ self.setCursor(Qt.ArrowCursor) def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent): """ Right click on node to show node menu Except on wallet node :param event: scene context menu event """ # no menu on the wallet node if self.status_wallet: return None # create node context menus self.menu = QMenu() # action show member QT_TRANSLATE_NOOP('WoT.Node', 'Informations') self.action_show_member = QAction( QCoreApplication.translate('WoT.Node', 'Informations'), self.scene()) self.menu.addAction(self.action_show_member) self.action_show_member.triggered.connect(self.member_action) # action add identity as contact QT_TRANSLATE_NOOP('WoT.Node', 'Add as contact') self.action_contact = QAction( QCoreApplication.translate('WoT.Node', 'Add as contact'), self.scene()) self.menu.addAction(self.action_contact) self.action_contact.triggered.connect(self.contact_action) # action transaction toward identity QT_TRANSLATE_NOOP('WoT.Node', 'Send money') self.action_transaction = QAction( QCoreApplication.translate('WoT.Node', 'Send money'), self.scene()) self.menu.addAction(self.action_transaction) self.action_transaction.triggered.connect(self.transaction_action) # action sign identity QT_TRANSLATE_NOOP('WoT.Node', 'Certify identity') self.action_sign = QAction( QCoreApplication.translate('WoT.Node', 'Certify identity'), self.scene()) self.menu.addAction(self.action_sign) self.action_sign.triggered.connect(self.sign_action) # run menu self.menu.exec(event.screenPos()) def add_arc(self, arc): """ Add arc to the arc list :param arc: Arc """ self.arcs.append(arc) def member_action(self): """ Transaction action to identity node """ # trigger scene signal self.scene().node_member.emit(self.metadata) def contact_action(self): """ Transaction action to identity node """ # trigger scene signal self.scene().node_contact.emit(self.metadata) def sign_action(self): """ Sign identity node """ # trigger scene signal self.scene().node_signed.emit(self.metadata) def transaction_action(self): """ Transaction action to identity node """ # trigger scene signal self.scene().node_transaction.emit(self.metadata)
def refresh(self): if not self._mdlPlots or not self._mdlOutline or not self._mdlCharacter: return if not self.isVisible(): return LINE_HEIGHT = 18 SPACING = 3 TEXT_WIDTH = self.sldTxtSize.value() CIRCLE_WIDTH = 10 LEVEL_HEIGHT = 12 s = self.scene s.clear() # Get Max Level (max depth) root = self._mdlOutline.rootItem def maxLevel(item, level=0, max=0): if level > max: max = level for c in item.children(): m = maxLevel(c, level + 1) if m > max: max = m return max MAX_LEVEL = maxLevel(root) # Get the list of tracked items (array of references) trackedItems = [] if self.actPlots.isChecked(): trackedItems += self.plotReferences() if self.actCharacters.isChecked(): trackedItems += self.charactersReferences() ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING) fm = QFontMetrics(s.font()) max_name = 0 for ref in trackedItems: name = references.title(ref) max_name = max(fm.width(name), max_name) TITLE_WIDTH = max_name + 2 * SPACING # Add Folders and Texts outline = OutlineRect(0, 0, 0, ROWS_HEIGHT + SPACING + MAX_LEVEL * LEVEL_HEIGHT) s.addItem(outline) outline.setPos(TITLE_WIDTH + SPACING, 0) refCircles = [ ] # a list of all references, to be added later on the lines # A Function to add a rect with centered elided text def addRectText(x, w, parent, text="", level=0, tooltip=""): deltaH = LEVEL_HEIGHT if level else 0 r = OutlineRect(0, 0, w, parent.rect().height() - deltaH, parent, title=text) r.setPos(x, deltaH) txt = QGraphicsSimpleTextItem(text, r) f = txt.font() f.setPointSize(8) fm = QFontMetricsF(f) elidedText = fm.elidedText(text, Qt.ElideMiddle, w) txt.setFont(f) txt.setText(elidedText) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) txt.setY(0) return r # A function to returns an item's width, by counting its children def itemWidth(item): if item.isFolder(): r = 0 for c in item.children(): r += itemWidth(c) return r or TEXT_WIDTH else: return TEXT_WIDTH def listItems(item, rect, level=0): delta = 0 for child in item.children(): w = itemWidth(child) if child.isFolder(): parent = addRectText(delta, w, rect, child.title(), level, tooltip=child.title()) parent.setToolTip( references.tooltip(references.textReference( child.ID()))) listItems(child, parent, level + 1) else: rectChild = addRectText(delta, TEXT_WIDTH, rect, "", level, tooltip=child.title()) rectChild.setToolTip( references.tooltip(references.textReference( child.ID()))) # Find tracked references in that scene (or parent folders) for ref in trackedItems: result = [] # Tests if POV scenePOV = False # Will hold true of character is POV of the current text, not containing folder if references.type(ref) == references.CharacterLetter: ID = references.ID(ref) c = child while c: if c.POV() == ID: result.append(c.ID()) if c == child: scenePOV = True c = c.parent() # Search in notes/references c = child while c: result += references.findReferencesTo( ref, c, recursive=False) c = c.parent() if result: ref2 = result[0] # Create a RefCircle with the reference c = RefCircle(TEXT_WIDTH / 2, -CIRCLE_WIDTH / 2, CIRCLE_WIDTH, ID=ref2, important=scenePOV) # Store it, with the position of that item, to display it on the line later on refCircles.append( (ref, c, rect.mapToItem(outline, rectChild.pos()))) delta += w listItems(root, outline) OUTLINE_WIDTH = itemWidth(root) # Add Tracked items i = 0 itemsRect = s.addRect(0, 0, 0, 0) itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING) # Set of colors for plots (as long as they don't have their own colors) colors = [ "#D97777", "#AE5F8C", "#D9A377", "#FFC2C2", "#FFDEC2", "#D2A0BC", "#7B0F0F", "#7B400F", "#620C3D", "#AA3939", "#AA6C39", "#882D61", "#4C0000", "#4C2200", "#3D0022", ] for ref in trackedItems: if references.type(ref) == references.CharacterLetter: color = self._mdlCharacter.getCharacterByID( references.ID(ref)).color() else: color = QColor(colors[i % len(colors)]) # Rect r = QGraphicsRectItem(0, 0, TITLE_WIDTH, LINE_HEIGHT, itemsRect) r.setPen(QPen(Qt.NoPen)) r.setBrush(QBrush(color)) r.setPos(0, i * LINE_HEIGHT + i * SPACING) r.setToolTip(references.tooltip(ref)) i += 1 # Text name = references.title(ref) txt = QGraphicsSimpleTextItem(name, r) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) # Line line = PlotLine(0, 0, OUTLINE_WIDTH + SPACING, 0) line.setPos(TITLE_WIDTH, r.mapToScene(r.rect().center()).y()) s.addItem(line) line.setPen(QPen(color, 5)) line.setToolTip(references.tooltip(ref)) # We add the circles / references to text, on the line for ref2, circle, pos in refCircles: if ref2 == ref: circle.setParentItem(line) circle.setPos(pos.x(), 0) # self.view.fitInView(0, 0, TOTAL_WIDTH, i * LINE_HEIGHT, Qt.KeepAspectRatioByExpanding) # KeepAspectRatio self.view.setSceneRect(0, 0, 0, 0)