Ejemplo n.º 1
0
class MoveHistoryView(QGraphicsView):
    """A widget containing a view of the move history."""
    def __init__(self, parent):
        QGraphicsView.__init__(self, parent)
        self.setMinimumWidth(180)
        self.setMaximumWidth(180)
        self.scene = QGraphicsScene()
        self.setScene(self.scene)
        self.text_item = QGraphicsSimpleTextItem()
        self.scene.addItem(self.text_item)
        self.show()

    def set_move_history(self, move_history):
        """
        Sets the move history according to a list of moves.

        :param move_history: List of half moves
        """
        text = ''
        half_move_number = 1
        full_move_number = 1
        for half_move in move_history:
            if half_move_number % 2 == 1:
                text += str(full_move_number) + '. ' + half_move
                half_move_number += 1
            else:
                text += ' ' + half_move + '\n'
                half_move_number += 1
                full_move_number += 1

        self.text_item.setText(text)
Ejemplo n.º 2
0
class GameStateView(QGraphicsView):
    """A widget displaying a view of the chess game state."""
    def __init__(self, parent):
        QGraphicsView.__init__(self, parent)
        self.setMinimumSize(180, 115)
        self.setMaximumSize(180, 115)
        self.scene = QGraphicsScene()
        self.setScene(self.scene)
        self.text_item = QGraphicsSimpleTextItem()
        self.scene.addItem(self.text_item)
        self.show()

    def set_game_state(self, game_state):
        """
        Updates the game state view with the provided values.

        :param game_state: Dictionary containing {topic:value} pairs
        """
        text = ''
        for topic in game_state:
            text += topic + ': ' + game_state[topic] + '\n'

        self.text_item.setText(text)
Ejemplo n.º 3
0
class ProjectItemIcon(QGraphicsRectItem):
    """Base class for project item icons drawn in Design View."""

    ITEM_EXTENT = 64

    def __init__(self, toolbox, icon_file, icon_color, background_color):
        """
        Args:
            toolbox (ToolboxUI): QMainWindow instance
            icon_file (str): Path to icon resource
            icon_color (QColor): Icon's color
            background_color (QColor): Background color
        """
        super().__init__()
        self._toolbox = toolbox
        self.icon_file = icon_file
        self._moved_on_scene = False
        self.previous_pos = QPointF()
        self.current_pos = QPointF()
        self.icon_group = {self}
        self.renderer = QSvgRenderer()
        self.svg_item = QGraphicsSvgItem(self)
        self.colorizer = QGraphicsColorizeEffect()
        self.setRect(
            QRectF(-self.ITEM_EXTENT / 2, -self.ITEM_EXTENT / 2,
                   self.ITEM_EXTENT, self.ITEM_EXTENT))
        self.text_font_size = 10  # point size
        # Make item name graphics item.
        self._name = ""
        self.name_item = QGraphicsSimpleTextItem(self._name, self)
        self.set_name_attributes()  # Set font, size, position, etc.
        # Make connector buttons
        self.connectors = dict(
            bottom=ConnectorButton(self, toolbox, position="bottom"),
            left=ConnectorButton(self, toolbox, position="left"),
            right=ConnectorButton(self, toolbox, position="right"),
        )
        # Make exclamation and rank icons
        self.exclamation_icon = ExclamationIcon(self)
        self.execution_icon = ExecutionIcon(self)
        self.rank_icon = RankIcon(self)
        brush = QBrush(background_color)
        self._setup(brush, icon_file, icon_color)
        shadow_effect = QGraphicsDropShadowEffect()
        shadow_effect.setOffset(1)
        shadow_effect.setEnabled(False)
        self.setGraphicsEffect(shadow_effect)

    def finalize(self, name, x, y):
        """
        Names the icon and moves it by given amount.

        Args:
            name (str): icon's name
            x (int): horizontal offset
            y (int): vertical offset
        """
        self.update_name_item(name)
        self.moveBy(x, y)

    def _setup(self, brush, svg, svg_color):
        """Setup item's attributes.

        Args:
            brush (QBrush): Used in filling the background rectangle
            svg (str): Path to SVG icon file
            svg_color (QColor): Color of SVG icon
        """
        self.setPen(QPen(Qt.black, 1, Qt.SolidLine))
        self.setBrush(brush)
        self.colorizer.setColor(svg_color)
        # Load SVG
        loading_ok = self.renderer.load(svg)
        if not loading_ok:
            self._toolbox.msg_error.emit(
                "Loading SVG icon from resource:{0} failed".format(svg))
            return
        size = self.renderer.defaultSize()
        self.svg_item.setSharedRenderer(self.renderer)
        self.svg_item.setElementId(
            "")  # guess empty string loads the whole file
        dim_max = max(size.width(), size.height())
        rect_w = self.rect().width()  # Parent rect width
        margin = 32
        self.svg_item.setScale((rect_w - margin) / dim_max)
        self.svg_item.setPos(self.rect().center() -
                             self.svg_item.sceneBoundingRect().center())
        self.svg_item.setGraphicsEffect(self.colorizer)
        self.setFlag(QGraphicsItem.ItemIsMovable, enabled=True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True)
        self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True)
        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, enabled=True)
        self.setAcceptHoverEvents(True)
        self.setCursor(Qt.PointingHandCursor)
        # Set exclamation, execution_log, and rank icons position
        self.exclamation_icon.setPos(
            self.rect().topRight() -
            self.exclamation_icon.sceneBoundingRect().topRight())
        self.execution_icon.setPos(
            self.rect().bottomRight() -
            0.5 * self.execution_icon.sceneBoundingRect().bottomRight())
        self.rank_icon.setPos(self.rect().topLeft())

    def name(self):
        """Returns name of the item that is represented by this icon.

        Returns:
            str: icon's name
        """
        return self._name

    def update_name_item(self, new_name):
        """Set a new text to name item.

        Args:
            new_name (str): icon's name
        """
        self._name = new_name
        self.name_item.setText(new_name)
        self.set_name_attributes()

    def set_name_attributes(self):
        """Set name QGraphicsSimpleTextItem attributes (font, size, position, etc.)"""
        # Set font size and style
        font = self.name_item.font()
        font.setPointSize(self.text_font_size)
        font.setBold(True)
        self.name_item.setFont(font)
        # Set name item position (centered on top of the master icon)
        name_width = self.name_item.boundingRect().width()
        name_height = self.name_item.boundingRect().height()
        self.name_item.setPos(
            self.rect().x() + self.rect().width() / 2 - name_width / 2,
            self.rect().y() - name_height - 4)

    def conn_button(self, position="left"):
        """Returns item's connector button.

        Args:
            position (str): "left", "right" or "bottom"

        Returns:
            QWidget: connector button
        """
        return self.connectors.get(position, self.connectors["left"])

    def outgoing_links(self):
        """Collects outgoing links.

        Returns:
            list of LinkBase: outgoing links
        """
        return [
            l for conn in self.connectors.values()
            for l in conn.outgoing_links()
        ]

    def incoming_links(self):
        """Collects incoming links.

        Returns:
            list of LinkBase: outgoing links
        """
        return [
            l for conn in self.connectors.values()
            for l in conn.incoming_links()
        ]

    def run_execution_leave_animation(self, skipped):
        """
        Starts the animation associated with execution leaving the icon.

        Args:
            skipped (bool): True if project item was not actually executed.
        """
        animation_group = QParallelAnimationGroup(self._toolbox)
        for link in self.outgoing_links():
            animation_group.addAnimation(
                link.make_execution_animation(skipped))
        animation_group.start()

    def hoverEnterEvent(self, event):
        """Sets a drop shadow effect to icon when mouse enters its boundaries.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        self.prepareGeometryChange()
        self.graphicsEffect().setEnabled(True)
        event.accept()

    def hoverLeaveEvent(self, event):
        """Disables the drop shadow when mouse leaves icon boundaries.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        self.prepareGeometryChange()
        self.graphicsEffect().setEnabled(False)
        event.accept()

    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        self.icon_group = set(x for x in self.scene().selectedItems()
                              if isinstance(x, ProjectItemIcon)) | {self}
        for icon in self.icon_group:
            icon.previous_pos = icon.scenePos()

    def mouseMoveEvent(self, event):
        """Moves icon(s) while the mouse button is pressed.
        Update links that are connected to selected icons.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        super().mouseMoveEvent(event)
        self.update_links_geometry()

    def moveBy(self, dx, dy):
        super().moveBy(dx, dy)
        self.update_links_geometry()

    def update_links_geometry(self):
        """Updates geometry of connected links to reflect this item's most recent position."""
        links = set(link for icon in self.icon_group
                    for conn in icon.connectors.values()
                    for link in conn.links)
        for link in links:
            link.update_geometry()

    def mouseReleaseEvent(self, event):
        for icon in self.icon_group:
            icon.current_pos = icon.scenePos()
        # pylint: disable=undefined-variable
        if (self.current_pos - self.previous_pos
            ).manhattanLength() > qApp.startDragDistance():
            self._toolbox.undo_stack.push(
                MoveIconCommand(self, self._toolbox.project()))
        super().mouseReleaseEvent(event)

    def notify_item_move(self):
        if self._moved_on_scene:
            self._moved_on_scene = False
            scene = self.scene()
            scene.item_move_finished.emit(self)

    def contextMenuEvent(self, event):
        """Show item context menu.

        Args:
            event (QGraphicsSceneMouseEvent): Mouse event
        """
        event.accept()
        self.scene().clearSelection()
        self.setSelected(True)
        ind = self._toolbox.project_item_model.find_item(self.name())
        self._toolbox.show_project_item_context_menu(event.screenPos(), ind)

    def itemChange(self, change, value):
        """
        Reacts to item removal and position changes.

        In particular, destroys the drop shadow effect when the items is removed from a scene
        and keeps track of item's movements on the scene.

        Args:
            change (GraphicsItemChange): a flag signalling the type of the change
            value: a value related to the change

        Returns:
             Whatever super() does with the value parameter
        """
        if change == QGraphicsItem.ItemScenePositionHasChanged:
            self._moved_on_scene = True
        elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None:
            self.prepareGeometryChange()
            self.setGraphicsEffect(None)
        return super().itemChange(change, value)

    def select_item(self):
        """Update GUI to show the details of the selected item."""
        ind = self._toolbox.project_item_model.find_item(self.name())
        self._toolbox.ui.treeView_project.setCurrentIndex(ind)
Ejemplo n.º 4
0
class QInstruction(QCachedGraphicsItem):

    GRAPH_ADDR_SPACING = 20
    GRAPH_MNEMONIC_SPACING = 10
    GRAPH_OPERAND_SPACING = 2
    GRAPH_COMMENT_STRING_SPACING = 10

    INTERSPERSE_ARGS = ', '

    LINEAR_INSTRUCTION_OFFSET = 120
    COMMENT_PREFIX = "// "

    def __init__(self,
                 workspace,
                 func_addr,
                 disasm_view,
                 disasm,
                 infodock,
                 insn,
                 out_branch,
                 config,
                 parent=None):
        super().__init__(parent=parent)

        # initialization
        self.workspace = workspace
        self.func_addr = func_addr
        self.disasm_view = disasm_view
        self.disasm = disasm
        self.infodock = infodock
        self.variable_manager = infodock.variable_manager
        self.insn = insn
        self.out_branch = out_branch
        self._config = config

        # all "widgets"
        self._addr = None
        self._mnemonic = None
        self._addr_item: QGraphicsSimpleTextItem = None
        self._mnemonic_item: QGraphicsSimpleTextItem = None
        self._operands: List[QOperand] = []
        self._commas: List[QGraphicsSimpleTextItem] = []
        self._string = None
        self._string_item: Optional[QGraphicsSimpleTextItem] = None
        self._comment = None
        self._comment_items: Optional[
            List[QGraphicsSimpleTextItem]] = None  # one comment per line
        self._legend = None
        self._width = 0
        self._height = 0

        self._init_widgets()

    def contextMenuEvent(
            self,
            event: PySide2.QtWidgets.QGraphicsSceneContextMenuEvent) -> None:
        pass

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
        if self.workspace.plugins.handle_click_insn(self, event):
            # stop handling this event if the event has been handled by a plugin
            event.accept()
        elif event.button(
        ) == Qt.LeftButton and QApplication.keyboardModifiers() in (
                Qt.NoModifier, Qt.ControlModifier):
            # toggle selection
            self.infodock.toggle_instruction_selection(
                self.addr,
                insn_pos=self.scenePos(),
                unique=QApplication.keyboardModifiers() != Qt.ControlModifier)
            event.accept()
        elif event.button(
        ) == Qt.RightButton and QApplication.keyboardModifiers(
        ) == Qt.NoModifier:
            if self.addr not in self.infodock.selected_insns:
                self.infodock.toggle_instruction_selection(
                    self.addr, insn_pos=self.scenePos(), unique=True)
            self.disasm_view.instruction_context_menu(self.insn, QCursor.pos())
            event.accept()
        else:
            super().mousePressEvent(event)

    @property
    def addr(self):
        return self.insn.addr

    def _calc_backcolor(self):
        # First we'll check for customizations
        color = self.workspace.plugins.color_insn(self.insn.addr,
                                                  self.selected)
        if color is not None:
            return color

        if self.selected:
            return self._config.disasm_view_node_instruction_selected_background_color

        return None  # None here means transparent, reusing the block color

    @property
    def selected(self):
        """
        If this instruction is selected or not.

        :return:    True if it is selected, False otherwise.
        :rtype:     bool
        """

        return self.infodock.is_instruction_selected(self.addr)

    def clear_cache(self):
        super().clear_cache()
        for obj in self._operands:
            obj.clear_cache()

    def refresh(self):
        self.load_comment()
        self._init_comments_or_string()

        for operand in self._operands:
            operand.refresh()

        self._layout_items_and_update_size()
        self.recalculate_size()

    def get_operand(self, operand_idx):
        if operand_idx < len(self._operands):
            return self._operands[operand_idx]
        return None

    def load_comment(self):
        self._comment = get_comment_for_display(self.workspace.instance.kb,
                                                self.insn.addr)

    def paint(self, painter, option, widget):  # pylint: disable=unused-argument

        painter.setRenderHints(QPainter.Antialiasing
                               | QPainter.SmoothPixmapTransform
                               | QPainter.HighQualityAntialiasing)

        # background color
        backcolor = self._calc_backcolor()
        if backcolor is not None:
            painter.setBrush(backcolor)
            painter.setPen(backcolor)
            painter.drawRect(0, 0, self.width, self.height)

        # any plugin instruction rendering passes
        self.workspace.plugins.draw_insn(self, painter)

    #
    # Private methods
    #

    def _init_widgets(self):

        self.load_comment()
        self._operands.clear()

        # address
        self._addr = "%08x" % self.insn.addr
        self._addr_item = QGraphicsSimpleTextItem(self)
        self._addr_item.setBrush(
            QBrush(self._config.disasm_view_node_address_color))
        self._addr_item.setFont(self._config.disasm_font)
        self._addr_item.setText(self._addr)

        # mnemonic
        self._mnemonic = self.insn.mnemonic.render()[0]
        self._mnemonic_item = QGraphicsSimpleTextItem(self)
        self._mnemonic_item.setFont(self._config.disasm_font)
        self._mnemonic_item.setBrush(
            self._config.disasm_view_node_mnemonic_color)
        self._mnemonic_item.setText(self._mnemonic)

        # operands
        for i, operand in enumerate(self.insn.operands):
            is_branch_target = self.insn.type in (
                'branch', 'call') and i == self.insn.branch_target_operand
            is_indirect_branch = self.insn.branch_type == 'indirect'
            branch_targets = None
            if is_branch_target:
                if self.out_branch is not None:
                    branch_targets = self.out_branch.targets
                else:
                    # it does not create multiple branches. e.g., a call instruction
                    if len(operand.children) == 1 and type(
                            operand.children[0]) is Value:
                        branch_targets = (operand.children[0].val, )
            qoperand = QOperand(self.workspace,
                                self.func_addr,
                                self.disasm_view,
                                self.disasm,
                                self.infodock,
                                self.insn,
                                operand,
                                i,
                                is_branch_target,
                                is_indirect_branch,
                                branch_targets,
                                self._config,
                                parent=self)
            self._operands.append(qoperand)

        # all commas
        for _ in range(len(self._operands) - 1):
            comma = QGraphicsSimpleTextItem(self.INTERSPERSE_ARGS, self)
            comma.setFont(self._config.disasm_font)
            comma.setBrush(self._config.disasm_view_node_mnemonic_color)
            self._commas.append(comma)

        if should_display_string_label(self.workspace.instance.cfg,
                                       self.insn.addr,
                                       self.workspace.instance.project):
            # yes we should display a string label
            self._string = get_string_for_display(
                self.workspace.instance.cfg, self.insn.addr,
                self.workspace.instance.project)
            if self._string is None:
                self._string = "<Unknown>"

        self._init_comments_or_string()

        self._layout_items_and_update_size()

    def _init_comments_or_string(self):

        # remove existing comments or strings
        if self._comment_items:
            for comm in self._comment_items:
                comm: QGraphicsSimpleTextItem
                comm.setParentItem(None)
            self._comment_items = None
        elif self._string_item is not None:
            self._string_item.setParentItem(None)
            self._string_item = None

        # comment or string - comments have precedence
        if self._comment:
            self._string_item = None
            lines = self._comment.split('\n')
            self._comment_items = []
            for line in lines:
                comment = QGraphicsSimpleTextItem(self.COMMENT_PREFIX + line,
                                                  self)
                comment.setFont(self._config.disasm_font)
                comment.setBrush(
                    Qt.darkGreen)  # TODO: Expose it as a setting in Config
                self._comment_items.append(comment)
        elif self._string is not None:
            self._comment_items = None
            self._string_item = QGraphicsSimpleTextItem(self._string, self)
            self._string_item.setFont(self._config.disasm_font)
            self._string_item.setBrush(
                Qt.gray)  # TODO: Expose it as a setting in Config

    def _layout_items_and_update_size(self):

        x, y = 0, 0

        # address
        if self.disasm_view.show_address:
            self._addr_item.setVisible(True)
            self._addr_item.setPos(x, y)
            x += self._addr_item.boundingRect().width(
            ) + self.GRAPH_ADDR_SPACING
        else:
            self._addr_item.setVisible(False)

        # mnemonic
        self._mnemonic_item.setPos(x, y)
        x += self._mnemonic_item.boundingRect().width(
        ) + self.GRAPH_MNEMONIC_SPACING

        # operands and commas
        for operand, comma in zip(self._operands, self._commas):
            operand.setPos(x, y)
            x += operand.boundingRect().width()
            comma.setPos(x, y)
            x += comma.boundingRect().width()

        # the last operand
        if self._operands:
            self._operands[-1].setPos(x, y)
            x += self._operands[-1].boundingRect().width()

        # comments and strings
        if self._comment_items:
            x += self.GRAPH_COMMENT_STRING_SPACING
            max_comment_width = 0
            for comment in self._comment_items:
                comment.setPos(x, y)
                max_comment_width = max(comment.boundingRect().width(),
                                        max_comment_width)
                y += comment.boundingRect().height()
            x += max_comment_width
        elif self._string_item is not None:
            x += self.GRAPH_COMMENT_STRING_SPACING
            self._string_item.setPos(x, y)
            x += self._string_item.boundingRect().width()
            y += self._string_item.boundingRect().height()
        else:
            y = self._mnemonic_item.boundingRect().height()

        # update size
        self._width = x
        self._height = y
        self.recalculate_size()

    def _boundingRect(self):
        return QRectF(0, 0, self._width, self._height)
Ejemplo n.º 5
0
class View(QGraphicsView):
    def __init__(self, parent=None):
        super(View, self).__init__(parent)
        self.setScene(QGraphicsScene(self))

        self.setDragMode(QGraphicsView.NoDrag)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # Chart
        self._chart = QtCharts.QChart()
        self._chart.setMinimumSize(640, 480)
        self._chart.setTitle("Hover the line to show callout. Click the line "
                             "to make it stay")
        self._chart.legend().hide()
        self.series = QtCharts.QLineSeries()
        self.series.append(1, 3)
        self.series.append(4, 5)
        self.series.append(5, 4.5)
        self.series.append(7, 1)
        self.series.append(11, 2)
        self._chart.addSeries(self.series)

        self.series2 = QtCharts.QSplineSeries()
        self.series2.append(1.6, 1.4)
        self.series2.append(2.4, 3.5)
        self.series2.append(3.7, 2.5)
        self.series2.append(7, 4)
        self.series2.append(10, 2)
        self._chart.addSeries(self.series2)

        self._chart.createDefaultAxes()
        self._chart.setAcceptHoverEvents(True)

        self.setRenderHint(QPainter.Antialiasing)
        self.scene().addItem(self._chart)

        self._coordX = QGraphicsSimpleTextItem(self._chart)
        self._coordX.setPos(self._chart.size().width() / 2 - 50,
                            self._chart.size().height())
        self._coordX.setText("X: ")
        self._coordY = QGraphicsSimpleTextItem(self._chart)
        self._coordY.setPos(self._chart.size().width() / 2 + 50,
                            self._chart.size().height())
        self._coordY.setText("Y: ")

        self._callouts = []
        self._tooltip = Callout(self._chart)

        self.series.clicked.connect(self.keepCallout)
        self.series.hovered.connect(self.tooltip)

        self.series2.clicked.connect(self.keepCallout)
        self.series2.hovered.connect(self.tooltip)

        self.setMouseTracking(True)

    def resizeEvent(self, event):
        if self.scene():
            self.scene().setSceneRect(QRectF(QPointF(0, 0), event.size()))
            self._chart.resize(event.size())
            self._coordX.setPos(self._chart.size().width() / 2 - 50,
                                self._chart.size().height() - 20)
            self._coordY.setPos(self._chart.size().width() / 2 + 50,
                                self._chart.size().height() - 20)
            for callout in self._callouts:
                callout.updateGeometry()
        QGraphicsView.resizeEvent(self, event)

    def mouseMoveEvent(self, event):
        self._coordX.setText("X: {0:.2f}".format(
            self._chart.mapToValue(event.pos()).x()))
        self._coordY.setText("Y: {0:.2f}".format(
            self._chart.mapToValue(event.pos()).y()))
        QGraphicsView.mouseMoveEvent(self, event)

    def keepCallout(self):
        self._callouts.append(self._tooltip)
        self._tooltip = Callout(self._chart)

    def tooltip(self, point, state):
        if self._tooltip == 0:
            self._tooltip = Callout(self._chart)

        if state:
            self._tooltip.setText("X: {0:.2f} \nY: {1:.2f} ".format(
                point.x(), point.y()))
            self._tooltip.setAnchor(point)
            self._tooltip.setZValue(11)
            self._tooltip.updateGeometry()
            self._tooltip.show()
        else:
            self._tooltip.hide()
Ejemplo n.º 6
0
class ProjectItemIcon(QGraphicsRectItem):

    ITEM_EXTENT = 64

    def __init__(self, toolbox, x, y, project_item, icon_file, icon_color,
                 background_color):
        """Base class for project item icons drawn in Design View.

        Args:
            toolbox (ToolBoxUI): QMainWindow instance
            x (float): Icon x coordinate
            y (float): Icon y coordinate
            project_item (ProjectItem): Item
            icon_file (str): Path to icon resource
            icon_color (QColor): Icon's color
            background_color (QColor): Background color
        """
        super().__init__()
        self._toolbox = toolbox
        self._project_item = project_item
        self._moved_on_scene = False
        self._previous_pos = QPointF()
        self._current_pos = QPointF()
        self.icon_group = {self}
        self.renderer = QSvgRenderer()
        self.svg_item = QGraphicsSvgItem(self)
        self.colorizer = QGraphicsColorizeEffect()
        self.setRect(
            QRectF(x - self.ITEM_EXTENT / 2, y - self.ITEM_EXTENT / 2,
                   self.ITEM_EXTENT, self.ITEM_EXTENT))
        self.text_font_size = 10  # point size
        # Make item name graphics item.
        name = project_item.name if project_item else ""
        self.name_item = QGraphicsSimpleTextItem(name, self)
        self.set_name_attributes()  # Set font, size, position, etc.
        # Make connector buttons
        self.connectors = dict(
            bottom=ConnectorButton(self, toolbox, position="bottom"),
            left=ConnectorButton(self, toolbox, position="left"),
            right=ConnectorButton(self, toolbox, position="right"),
        )
        # Make exclamation and rank icons
        self.exclamation_icon = ExclamationIcon(self)
        self.rank_icon = RankIcon(self)
        brush = QBrush(background_color)
        self._setup(brush, icon_file, icon_color)
        self.activate()

    def activate(self):
        """Adds items to scene and setup graphics effect.
        Called in the constructor and when re-adding the item to the project in the context of undo/redo.
        """
        scene = self._toolbox.ui.graphicsView.scene()
        scene.addItem(self)
        shadow_effect = QGraphicsDropShadowEffect()
        shadow_effect.setOffset(1)
        shadow_effect.setEnabled(False)
        self.setGraphicsEffect(shadow_effect)

    def _setup(self, brush, svg, svg_color):
        """Setup item's attributes.

        Args:
            brush (QBrush): Used in filling the background rectangle
            svg (str): Path to SVG icon file
            svg_color (QColor): Color of SVG icon
        """
        self.setPen(QPen(Qt.black, 1, Qt.SolidLine))
        self.setBrush(brush)
        self.colorizer.setColor(svg_color)
        # Load SVG
        loading_ok = self.renderer.load(svg)
        if not loading_ok:
            self._toolbox.msg_error.emit(
                "Loading SVG icon from resource:{0} failed".format(svg))
            return
        size = self.renderer.defaultSize()
        self.svg_item.setSharedRenderer(self.renderer)
        self.svg_item.setElementId(
            "")  # guess empty string loads the whole file
        dim_max = max(size.width(), size.height())
        rect_w = self.rect().width()  # Parent rect width
        margin = 32
        self.svg_item.setScale((rect_w - margin) / dim_max)
        x_offset = (rect_w - self.svg_item.sceneBoundingRect().width()) / 2
        y_offset = (rect_w - self.svg_item.sceneBoundingRect().height()) / 2
        self.svg_item.setPos(self.rect().x() + x_offset,
                             self.rect().y() + y_offset)
        self.svg_item.setGraphicsEffect(self.colorizer)
        self.setFlag(QGraphicsItem.ItemIsMovable, enabled=True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True)
        self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True)
        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, enabled=True)
        self.setAcceptHoverEvents(True)
        self.setCursor(Qt.PointingHandCursor)
        # Set exclamation and rank icons position
        self.exclamation_icon.setPos(
            self.rect().topRight() -
            self.exclamation_icon.sceneBoundingRect().topRight())
        self.rank_icon.setPos(self.rect().topLeft())

    def name(self):
        """Returns name of the item that is represented by this icon."""
        return self._project_item.name

    def update_name_item(self, new_name):
        """Set a new text to name item. Used when a project item is renamed."""
        self.name_item.setText(new_name)
        self.set_name_attributes()

    def set_name_attributes(self):
        """Set name QGraphicsSimpleTextItem attributes (font, size, position, etc.)"""
        # Set font size and style
        font = self.name_item.font()
        font.setPointSize(self.text_font_size)
        font.setBold(True)
        self.name_item.setFont(font)
        # Set name item position (centered on top of the master icon)
        name_width = self.name_item.boundingRect().width()
        name_height = self.name_item.boundingRect().height()
        self.name_item.setPos(
            self.rect().x() + self.rect().width() / 2 - name_width / 2,
            self.rect().y() - name_height - 4)

    def conn_button(self, position="left"):
        """Returns items connector button (QWidget)."""
        return self.connectors.get(position, self.connectors["left"])

    def outgoing_links(self):
        return [
            l for conn in self.connectors.values()
            for l in conn.outgoing_links()
        ]

    def incoming_links(self):
        return [
            l for conn in self.connectors.values()
            for l in conn.incoming_links()
        ]

    def hoverEnterEvent(self, event):
        """Sets a drop shadow effect to icon when mouse enters its boundaries.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        self.prepareGeometryChange()
        self.graphicsEffect().setEnabled(True)
        event.accept()

    def hoverLeaveEvent(self, event):
        """Disables the drop shadow when mouse leaves icon boundaries.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        self.prepareGeometryChange()
        self.graphicsEffect().setEnabled(False)
        event.accept()

    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        self.icon_group = set(x for x in self.scene().selectedItems()
                              if isinstance(x, ProjectItemIcon)) | {self}
        for icon in self.icon_group:
            icon._previous_pos = icon.scenePos()

    def mouseMoveEvent(self, event):
        """Moves icon(s) while the mouse button is pressed.
        Update links that are connected to selected icons.

        Args:
            event (QGraphicsSceneMouseEvent): Event
        """
        super().mouseMoveEvent(event)
        self.update_links_geometry()

    def moveBy(self, dx, dy):
        super().moveBy(dx, dy)
        self.update_links_geometry()

    def update_links_geometry(self):
        """Updates geometry of connected links to reflect this item's most recent position."""
        links = set(link for icon in self.icon_group
                    for conn in icon.connectors.values()
                    for link in conn.links)
        for link in links:
            link.update_geometry()

    def mouseReleaseEvent(self, event):
        for icon in self.icon_group:
            icon._current_pos = icon.scenePos()
        # pylint: disable=undefined-variable
        if (self._current_pos - self._previous_pos
            ).manhattanLength() > qApp.startDragDistance():
            self._toolbox.undo_stack.push(MoveIconCommand(self))
        super().mouseReleaseEvent(event)

    def notify_item_move(self):
        if self._moved_on_scene:
            self._moved_on_scene = False
            scene = self.scene()
            scene.item_move_finished.emit(self)

    def contextMenuEvent(self, event):
        """Show item context menu.

        Args:
            event (QGraphicsSceneMouseEvent): Mouse event
        """
        self.scene().clearSelection()
        self.setSelected(True)
        self._toolbox.show_item_image_context_menu(event.screenPos(),
                                                   self.name())

    def keyPressEvent(self, event):
        """Handles deleting and rotating the selected
        item when dedicated keys are pressed.

        Args:
            event (QKeyEvent): Key event
        """
        if event.key() == Qt.Key_Delete and self.isSelected():
            self._project_item._project.remove_item(self.name())
            event.accept()
        elif event.key() == Qt.Key_R and self.isSelected():
            # TODO:
            # 1. Change name item text direction when rotating
            # 2. Save rotation into project file
            rect = self.mapToScene(self.boundingRect()).boundingRect()
            center = rect.center()
            t = QTransform()
            t.translate(center.x(), center.y())
            t.rotate(90)
            t.translate(-center.x(), -center.y())
            self.setPos(t.map(self.pos()))
            self.setRotation(self.rotation() + 90)
            links = set(lnk for conn in self.connectors.values()
                        for lnk in conn.links)
            for link in links:
                link.update_geometry()
            event.accept()
        else:
            super().keyPressEvent(event)

    def itemChange(self, change, value):
        """
        Reacts to item removal and position changes.

        In particular, destroys the drop shadow effect when the items is removed from a scene
        and keeps track of item's movements on the scene.

        Args:
            change (GraphicsItemChange): a flag signalling the type of the change
            value: a value related to the change

        Returns:
             Whatever super() does with the value parameter
        """
        if change == QGraphicsItem.ItemScenePositionHasChanged:
            self._moved_on_scene = True
        elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None:
            self.prepareGeometryChange()
            self.setGraphicsEffect(None)
        return super().itemChange(change, value)

    def show_item_info(self):
        """Update GUI to show the details of the selected item."""
        ind = self._toolbox.project_item_model.find_item(self.name())
        self._toolbox.ui.treeView_project.setCurrentIndex(ind)
Ejemplo n.º 7
0
class View(QGraphicsView):
    def __init__(self, parent = None):
        super(View, self).__init__(parent)
        self.setScene(QGraphicsScene(self))

        self.setDragMode(QGraphicsView.NoDrag)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # Chart
        self._chart = QtCharts.QChart()
        self._chart.setMinimumSize(640, 480)
        self._chart.setTitle("Hover the line to show callout. Click the line "
            "to make it stay")
        self._chart.legend().hide()
        self.series = QtCharts.QLineSeries()
        self.series.append(1, 3)
        self.series.append(4, 5)
        self.series.append(5, 4.5)
        self.series.append(7, 1)
        self.series.append(11, 2)
        self._chart.addSeries(self.series)

        self.series2 = QtCharts.QSplineSeries()
        self.series2.append(1.6, 1.4)
        self.series2.append(2.4, 3.5)
        self.series2.append(3.7, 2.5)
        self.series2.append(7, 4)
        self.series2.append(10, 2)
        self._chart.addSeries(self.series2)

        self._chart.createDefaultAxes()
        self._chart.setAcceptHoverEvents(True)

        self.setRenderHint(QPainter.Antialiasing)
        self.scene().addItem(self._chart)

        self._coordX = QGraphicsSimpleTextItem(self._chart)
        self._coordX.setPos(
            self._chart.size().width()/2 - 50, self._chart.size().height())
        self._coordX.setText("X: ");
        self._coordY = QGraphicsSimpleTextItem(self._chart)
        self._coordY.setPos(
            self._chart.size().width()/2 + 50, self._chart.size().height())
        self._coordY.setText("Y: ")

        self._callouts = []
        self._tooltip = Callout(self._chart)

        self.series.clicked.connect(self.keepCallout)
        self.series.hovered.connect(self.tooltip)

        self.series2.clicked.connect(self.keepCallout)
        self.series2.hovered.connect(self.tooltip)

        self.setMouseTracking(True)

    def resizeEvent(self, event):
        if self.scene():
            self.scene().setSceneRect(QRectF(QPointF(0, 0), event.size()))
            self._chart.resize(event.size())
            self._coordX.setPos(
                self._chart.size().width()/2 - 50,
                self._chart.size().height() - 20)
            self._coordY.setPos(
                self._chart.size().width()/2 + 50,
                self._chart.size().height() - 20)
            for callout in self._callouts:
                callout.updateGeometry()
        QGraphicsView.resizeEvent(self, event)


    def mouseMoveEvent(self, event):
        self._coordX.setText("X: {0:.2f}"
            .format(self._chart.mapToValue(event.pos()).x()))
        self._coordY.setText("Y: {0:.2f}"
            .format(self._chart.mapToValue(event.pos()).y()))
        QGraphicsView.mouseMoveEvent(self, event)

    def keepCallout(self):
        self._callouts.append(self._tooltip);
        self._tooltip = Callout(self._chart)

    def tooltip(self, point, state):
        if self._tooltip == 0:
            self._tooltip = Callout(self._chart)

        if state:
            self._tooltip.setText("X: {0:.2f} \nY: {1:.2f} "
                .format(point.x(),point.y()))
            self._tooltip.setAnchor(point)
            self._tooltip.setZValue(11)
            self._tooltip.updateGeometry()
            self._tooltip.show()
        else:
            self._tooltip.hide()
Ejemplo n.º 8
0
class InteractiveChartView(QtCharts.QChartView):
    """ A ChartView which optionally supports value tooltip, mouse position tracker, zoom and pan """
    def __init__(self,
                 chart: QtCharts.QChart = None,
                 parent: QWidget = None,
                 setInWindow: bool = False):
        super().__init__(parent)
        self._setInWindow: bool = setInWindow
        self._coordX: QGraphicsItem = None
        self._coordY: QGraphicsItem = None
        # self.__callouts: List[Callout] = None # Disabled for now
        self._tooltip: Callout = None
        # Internal fields
        self._mousePressEventPos: QPointF = None
        self._panOn: bool = False
        self._chartIsSet: bool = False  # True iff a valid (i.e. non empty) chart is set in this view
        # Option enable flags
        self._panEnabled: bool = True
        self._zoomEnabled: bool = True
        self._keySeqEnabled: bool = True
        self._calloutEnabled: bool = True
        self._positionTrackerEnabled: bool = True
        self._openInWindowDoubleClick: bool = True

        self.setDragMode(QGraphicsView.NoDrag)
        self.setRubberBand(QtCharts.QChartView.RectangleRubberBand)
        self.setMouseTracking(True)
        self.setInteractive(True)
        if chart:
            self.setChart(chart)

    def enablePan(self, value: bool) -> None:
        self._panEnabled = value

    def enableZoom(self, value: bool) -> None:
        self._zoomEnabled = value
        if value:
            self.setRubberBand(QtCharts.QChartView.RectangleRubberBand)
        else:
            self.setRubberBand(QtCharts.QChartView.NoRubberBand)

    def enableKeySequences(self, value: bool) -> None:
        self._keySeqEnabled = value

    def enableCallout(self, value: bool) -> None:
        self._calloutEnabled = value

    def enablePositionTracker(self, value: bool) -> None:
        self._positionTrackerEnabled = value

    def enableInWindow(self, value: bool) -> None:
        self._openInWindowDoubleClick = value

    def setChart(self, chart: QtCharts.QChart) -> None:
        """ Sets a new chart in the view. Doesn't delete the previous chart """
        # Set New chart
        super().setChart(chart)
        # Update fields
        series: List[QtCharts.QAbstractSeries] = chart.series()
        if not series:
            # Empty chart
            self._chartIsSet = False
        else:
            self._chartIsSet = True

        # self.__callouts = list()
        self._tooltip = Callout(chart)
        chart.setAcceptHoverEvents(True)
        for s in series:
            # s.clicked.connect(self.keepCallout)
            s.hovered.connect(self.tooltip)

        if self._chartIsSet and self._positionTrackerEnabled:
            self._coordX = QGraphicsSimpleTextItem(chart)
            self._coordX.setText("X: ")
            self._coordY = QGraphicsSimpleTextItem(chart)
            self._coordY.setText("Y: ")
            self._updateMouseTrackerPosition(
            )  # Show them in the correct place

    @staticmethod
    def _updateAxisTickCount(chart: QtCharts.QChart,
                             axis: QtCharts.QAbstractAxis,
                             newSize: QSize) -> None:
        """ Given an axis and the size of the view, sets the number of ticks to the best value
        avoiding too many overlapping labels """
        # Get one label as string and the current number of ticks/labels
        label: str
        ticks: int
        if axis.type() == QtCharts.QAbstractAxis.AxisTypeDateTime:
            ticks = axis.tickCount()  # current number of dates shown
            label = axis.min().toString(axis.format())
        elif axis.type() == QtCharts.QAbstractAxis.AxisTypeBarCategory:
            ticks = axis.count()  # number of labels
            label = axis.at(0) if ticks else None
        elif axis.type() == QtCharts.QAbstractAxis.AxisTypeCategory:
            ticks = axis.count()  # number of labels
            labels = axis.categoriesLabels()
            label = labels[0] if labels else None
        else:
            return  # Axis type not supported
        if not label:
            # No labels set
            return
        # Decide which dimension is relevant for resizing
        margins = chart.margins()
        # layoutMargins: (left, top, right, bottom)
        layoutMargins: Tuple[float, ...] = chart.layout().getContentsMargins()
        if layoutMargins:
            layoutMargins = tuple(
                [i if i is not None else 0.0 for i in layoutMargins])
        offset: int = 0
        if axis.orientation() == Qt.Horizontal:
            if margins:
                offset += margins.left() + margins.right()
            if layoutMargins:
                offset += layoutMargins[0] + layoutMargins[2]
            # 'length' is the available space for displaying labels, without margins and the space
            # between every label
            length = newSize.width() - offset - (ticks * 10)
        else:
            if margins:
                offset += margins.top() + margins.bottom()
            if layoutMargins:
                offset += layoutMargins[1] + layoutMargins[3]
            length = newSize.height() - offset - (ticks * 10)
        # Compute the optimal width of the label (in pixel)
        metrics = QFontMetrics(axis.labelsFont())
        optimalWidth: int = metrics.horizontalAdvance(
            label) * 1.9  # not precise, 1.9 is to fix it

        # Deal with every type separately
        if axis.type() == QtCharts.QAbstractAxis.AxisTypeDateTime:
            # Determine optimal number of ticks to avoid much overlapping
            newTicks = int(length / optimalWidth) - 1
            axis.setTickCount(newTicks)
        elif axis.type() == QtCharts.QAbstractAxis.AxisTypeCategory or axis.type() == \
                QtCharts.QAbstractAxis.AxisTypeBarCategory:
            labelSpace: float = length / (ticks * 2)
            if labelSpace < optimalWidth:
                deg = min([
                    90,
                    np.degrees(np.arccos(labelSpace / optimalWidth) * 1.1)
                ])
                axis.setLabelsAngle(deg)
            else:
                axis.setLabelsAngle(0)

    def setBestTickCount(self, newSize: QSize) -> None:
        if self._chartIsSet:
            xAxis = self.chart().axisX()
            yAxis = self.chart().axisY()
            if xAxis:
                self._updateAxisTickCount(self.chart(), xAxis, newSize)
            if yAxis:
                self._updateAxisTickCount(self.chart(), yAxis, newSize)

    def _updateMouseTrackerPosition(self,
                                    xOffset: int = 50,
                                    yOffset: int = 20) -> None:
        if self._chartIsSet and self._positionTrackerEnabled:
            # Update coordinates tracker position
            self._coordX.setPos(self.chart().size().width() / 2 - xOffset,
                                self.chart().size().height() - yOffset)
            self._coordY.setPos(self.chart().size().width() / 2 + xOffset,
                                self.chart().size().height() - yOffset)

    def resizeEvent(self, event: QResizeEvent):
        if self.scene() and self._chartIsSet:
            self.scene().setSceneRect(QRectF(QPointF(0, 0), event.size()))
            self.chart().resize(event.size())
            # Update axis
            self.setBestTickCount(event.size())
            # Update coordinates tracker position (if tracker is active)
            self._updateMouseTrackerPosition()
        super().resizeEvent(event)

    def mousePressEvent(self, event: QMouseEvent) -> None:
        if self._chartIsSet and self._panEnabled and event.button(
        ) == Qt.MiddleButton:
            self._mousePressEventPos = event.pos()
            self._panOn = True
            QApplication.setOverrideCursor(QCursor(Qt.ClosedHandCursor))
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        if self._panEnabled and self._panOn:
            offset = event.pos() - self._mousePressEventPos
            self.chart().scroll(-offset.x(), offset.y())
            self._mousePressEventPos = event.pos()
            event.accept()
        elif self._chartIsSet and self._positionTrackerEnabled:
            metrics = QFontMetrics(self._coordX.font())
            xVal = self.chart().mapToValue(event.pos()).x()
            yVal = self.chart().mapToValue(event.pos()).y()
            # if self.chart().axisX().type() == QtCharts.QAbstractAxis.AxisTypeDateTime:
            xText: str = 'X: {}'.format(
                computeAxisValue(self.chart().axisX(), xVal))
            yText: str = 'Y: {}'.format(
                computeAxisValue(self.chart().axisY(), yVal))
            xSize = metrics.width(xText, -1)
            ySize = metrics.width(yText, -1)
            totSize = xSize + ySize
            self._updateMouseTrackerPosition(xOffset=(totSize // 2))
            self._coordX.setText(xText)
            self._coordY.setText(yText)
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        if self._panEnabled and self._panOn:
            self._panOn = False
            QApplication.restoreOverrideCursor()
        super().mouseReleaseEvent(event)

    def keyPressEvent(self, event: QKeyEvent) -> None:
        if not self._chartIsSet:
            return
        if event.key() == Qt.Key_Left:
            self.chart().scroll(-10, 0)
        elif event.key() == Qt.Key_Right:
            self.chart().scroll(+10, 0)
        elif event.key() == Qt.Key_Up:
            self.chart().scroll(0, +10)
        elif event.key() == Qt.Key_Down:
            self.chart().scroll(0, -10)
        elif self._keySeqEnabled and event.key(
        ) == Qt.Key_R and event.modifiers() & Qt.ControlModifier:
            self.chart().zoomReset()
        else:
            super().keyPressEvent(event)

    def wheelEvent(self, event: QWheelEvent) -> None:
        if self._chartIsSet and self._zoomEnabled:
            delta: int = event.angleDelta().y()
            factor = pow(1.25, delta / 240.0)
            self.chart().zoom(factor)
            event.accept()

    # @Slot() # Disabled because it causes too many problems
    # def keepCallout(self):
    #     if not self._calloutEnabled:
    #         return
    #     self.__callouts.append(self._tooltip)
    #     self._tooltip = Callout(self.chart())

    # @Slot()  # Disabled because it causes too many problems
    # def clearCallouts(self) -> None:
    #     for c in self.__callouts:
    #         self.scene().removeItem(c)
    #     self.__callouts = list()

    @Slot(QPointF, bool)
    def tooltip(self, point: QPointF, state: bool):
        if not self._calloutEnabled:
            return
        if not self._tooltip:
            self._tooltip = Callout(self.chart())
        if state:
            self._tooltip.setText('X: {} \nY: {} '.format(
                computeAxisValue(self.chart().axisX(), point.x()),
                computeAxisValue(self.chart().axisY(), point.y())))
            self._tooltip.setAnchor(point)
            self._tooltip.setZValue(11)
            self._tooltip.updateGeometry()
            self._tooltip.show()
        else:
            self._tooltip.hide()

    def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
        if self._chartIsSet and self._openInWindowDoubleClick and not self._setInWindow and \
                event.button() == Qt.LeftButton:
            chartWindow = InteractiveChartWindow(
                self)  # needs a parent to be kept alive
            # Open widget with plot
            chart = copyChart(self.chart())
            iView = InteractiveChartView(chart=chart, setInWindow=True)
            iView.enableKeySequences(False)
            iView.setRenderHints(self.renderHints())
            chartWindow.setAttribute(Qt.WA_DeleteOnClose, True)
            chartWindow.setCentralWidget(
                iView)  # window takes ownership of view
            chartWindow.resize(600, 500)
            chartWindow.show()
            event.accept()
        super().mouseDoubleClickEvent(event)