Exemple #1
0
    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)
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
    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)
Exemple #7
0
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
Exemple #9
0
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)

    """
Exemple #10
0
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)

    """
Exemple #11
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
Exemple #13
0
        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
Exemple #14
0
    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
Exemple #15
0
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
Exemple #16
0
 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)
Exemple #17
0
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)
Exemple #19
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()
Exemple #20
0
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
Exemple #21
0
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()
Exemple #22
0
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)
Exemple #24
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)
Exemple #25
0
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
Exemple #26
0
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()
Exemple #27
0
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())
Exemple #28
0
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)
Exemple #29
0
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())
Exemple #30
0
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)
Exemple #31
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())
Exemple #32
0
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()
Exemple #33
0
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)
Exemple #34
0
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)
Exemple #35
0
    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)