Example #1
0
    def draw_model_name(painter: QPainter, geom: NodeGeometry,
                        state: NodeState, model: NodeDataModel,
                        node_style: NodeStyle):
        """
        Draw model name

        Parameters
        ----------
        painter : QPainter
        geom : NodeGeometry
        state : NodeState
        model : NodeDataModel
        """
        if not model.caption_visible:
            return
        name = model.caption
        f = painter.font()
        f.setBold(True)
        metrics = QFontMetrics(f)
        rect = metrics.boundingRect(name)
        position = QPointF((geom.width - rect.width()) / 2.0,
                           (geom.spacing + geom.entry_height) / 3.0)
        painter.setFont(f)
        painter.setPen(node_style.font_color)
        painter.drawText(position, name)
        f.setBold(False)
        painter.setFont(f)
Example #2
0
    def draw_validation_rect(painter: QPainter, geom: NodeGeometry,
                             model: NodeDataModel,
                             graphics_object: NodeGraphicsObject,
                             node_style: NodeStyle):
        """
        Draw validation rect

        Parameters
        ----------
        painter : QPainter
        geom : NodeGeometry
        model : NodeDataModel
        graphics_object : NodeGraphicsObject
        node_style : NodeStyle
        """
        model_validation_state = model.validation_state()
        if model_validation_state == NodeValidationState.valid:
            return

        color = (node_style.selected_boundary_color
                 if graphics_object.isSelected()
                 else node_style.normal_boundary_color)

        if geom.hovered:
            p = QPen(color, node_style.hovered_pen_width)
        else:
            p = QPen(color, node_style.pen_width)

        painter.setPen(p)

        # Drawing the validation message background
        if model_validation_state == NodeValidationState.error:
            painter.setBrush(node_style.error_color)
        else:
            painter.setBrush(node_style.warning_color)

        radius = 3.0
        diam = node_style.connection_point_diameter
        boundary = QRectF(
            -diam,
            -diam + geom.height - geom.validation_height,
            2.0 * diam + geom.width,
            2.0 * diam + geom.validation_height,
        )
        painter.drawRoundedRect(boundary, radius, radius)
        painter.setBrush(Qt.gray)

        # Drawing the validation message itself
        error_msg = model.validation_message()
        f = painter.font()
        metrics = QFontMetrics(f)
        rect = metrics.boundingRect(error_msg)
        position = QPointF(
            (geom.width - rect.width()) / 2.0,
            geom.height - (geom.validation_height - diam) / 2.0
        )
        painter.setFont(f)
        painter.setPen(node_style.font_color)
        painter.drawText(position, error_msg)
Example #3
0
 def sizeHint(self):
     font_metrics = QFontMetrics(self.font())
     r = font_metrics.boundingRect(
         QRect(QPoint(0, 0), self.size()),
         Qt.TextWordWrap | Qt.ElideRight,
         self._text,
     )
     return QSize(self.width(), r.height())
    def drawDisplay(self, painter, option, rect, text):
        """
        Draw the displayed text. Override of QItemDelegate::drawDisplay.

        Args:
            painter (QPainter): the painter
            option (QStyleOptionViewItem): drawing options
            rect (QRect): cell rectangle
            text (QString): text to display
        """
        fontMetric = QFontMetrics(option.font)

        reducable = True
        boudingRectangle = fontMetric.boundingRect(text)
        while (boudingRectangle.width() > rect.width()) and (reducable):
            text, reducable = self.reduceNumorsStr(text)
            boudingRectangle = fontMetric.boundingRect(text)

        super(DrillItemDelegate, self).drawDisplay(painter, option, rect, text)
Example #5
0
def get_text_width(text) -> int:
    """Return the width required to render ``text`` (including rich text elements)."""
    if qtpy.PYSIDE2:
        from qtpy.QtGui import Qt as _Qt
    else:
        from qtpy.QtCore import Qt as _Qt

    if _Qt.mightBeRichText(text):
        doc = QTextDocument()
        doc.setHtml(text)
        return doc.size().width()
    else:
        fm = QFontMetrics(QFont("", 0))
        return fm.boundingRect(text).width() + 5
Example #6
0
class LabelEdit(QLineEdit):
    def __init__(self, value='', parent=None, get_pos=None):
        """Helper class to position LineEdits above the slider handle

        Parameters
        ----------
        value : str, optional
            starting value, by default ''
        parent : QRangeSliderPopup, optional
            required for proper label positioning above handle, by default None
        get_pos : callable, optional
            function that returns the position of the appropriate slider handle
            by default None
        """
        super().__init__(value, parent=parent)
        self.fm = QFontMetrics(QFont("", 0))
        self.setObjectName('slice_label')
        self.min_width = 30
        self.max_width = 200
        self.setCursor(Qt.IBeamCursor)
        self.setValidator(QDoubleValidator())
        self.textChanged.connect(self._on_text_changed)
        self._on_text_changed(value)

        self.get_pos = get_pos
        if parent is not None:
            self.min_width = 50
            self.slider = parent.slider
            self.setAlignment(Qt.AlignCenter)

    def _on_text_changed(self, text):
        # with non mono-spaced fonts, an "n-digit" number isn't always the same
        # width... so we convert all numbers to "n 8s" before measuring width
        # so as to avoid visual jitter in the width of the label
        width = self.fm.boundingRect('8' * len(text)).width() + 4
        width = max(self.min_width, min(width, self.max_width))
        if width > self.min_width:
            # don't ever make the label smaller ... it causes visual jitter
            self.min_width = width
        self.setFixedWidth(width)

    def update_position(self):
        x = self.get_pos() - self.width() / 2
        y = self.slider.handle_radius + 6
        self.move(QPoint(x, -y) + self.slider.pos())

    def mouseDoubleClickEvent(self, event):
        self.selectAll()
Example #7
0
    def _resize_axis_labels(self):
        """When any of the labels get updated, this method updates all label
        widths to the width of the longest label. This keeps the sliders
        left-aligned and allows the full label to be visible at all times,
        with minimal space, without setting stretch on the layout.
        """
        fm = QFontMetrics(QFont("", 0))
        labels = self.findChildren(QLineEdit, 'axis_label')
        newwidth = max([fm.boundingRect(lab.text()).width() for lab in labels])

        if any(self._displayed_sliders):
            # set maximum width to no more than 20% of slider width
            maxwidth = self.slider_widgets[0].width() * 0.2
            newwidth = min([newwidth, maxwidth])
        for labl in labels:
            labl.setFixedWidth(newwidth + 10)
Example #8
0
 def _resize_slice_labels(self):
     """When the size of any dimension changes, we want to resize all of the
     slice labels to width of the longest label, to keep all the sliders
     right aligned.  The width is determined by the number of digits in the
     largest dimensions, plus a little padding.
     """
     width = 0
     for ax, maxi in enumerate(self.dims.max_indices):
         if self._displayed_sliders[ax]:
             length = len(str(int(maxi)))
             if length > width:
                 width = length
     # gui width of a string of length `width`
     fm = QFontMetrics(QFont("", 0))
     width = fm.boundingRect("8" * width).width()
     for labl in self.findChildren(QWidget, 'slice_label'):
         labl.setFixedWidth(width + 6)
Example #9
0
def get_text_width(text) -> int:
    """Return the width required to render ``text``."""
    fm = QFontMetrics(QFont("", 0))
    return fm.boundingRect(text).width() + 5
Example #10
0
class NodeGeometry:
    def __init__(self, node: NodeBase):
        super().__init__()
        self._node = node
        self._model = node.model
        self._dragging_pos = QPointF(-1000, -1000)
        self._entry_width = 0
        self._entry_height = 20
        self._font_metrics = QFontMetrics(QFont())
        self._height = 150
        self._hovered = False
        self._input_port_width = 70
        self._output_port_width = 70
        self._spacing = 20
        self._style = node.style
        self._width = 100

        f = QFont()
        f.setBold(True)
        self._bold_font_metrics = QFontMetrics(f)

    @property
    def height(self) -> int:
        """
        Height

        Returns
        -------
        value : int
        """
        return self._height

    @height.setter
    def height(self, h: int):
        self._height = int(h)

    @property
    def width(self) -> int:
        """
        Width

        Returns
        -------
        value : int
        """
        return self._width

    @width.setter
    def width(self, width: int):
        """
        Set width

        Parameters
        ----------
        width : int
        """
        self._width = int(width)

    @property
    def entry_height(self) -> int:
        """
        Entry height

        Returns
        -------
        value : int
        """
        return self._entry_height

    @entry_height.setter
    def entry_height(self, h: int):
        """
        Set entry height

        Parameters
        ----------
        h : int
        """
        self._entry_height = int(h)

    @property
    def entry_width(self) -> int:
        """
        Entry width

        Returns
        -------
        value : int
        """
        return self._entry_width

    @entry_width.setter
    def entry_width(self, width: int):
        """
        Set entry width

        Parameters
        ----------
        width : int
        """
        self._entry_width = int(width)

    @property
    def spacing(self) -> int:
        """
        Spacing

        Returns
        -------
        value : int
        """
        return self._spacing

    @spacing.setter
    def spacing(self, s: int):
        """
        Set spacing

        Parameters
        ----------
        s : int
        """
        self._spacing = int(s)

    @property
    def hovered(self) -> bool:
        """
        Hovered

        Returns
        -------
        value : bool
        """
        return self._hovered

    @hovered.setter
    def hovered(self, h: int):
        """
        Set hovered

        Parameters
        ----------
        h : int
        """
        self._hovered = bool(h)

    @property
    def num_sources(self) -> int:
        """
        N sources

        Returns
        -------
        value : int
        """
        return self._model.num_ports[PortType.output]

    @property
    def num_sinks(self) -> int:
        """
        N sinks

        Returns
        -------
        value : int
        """
        return self._model.num_ports[PortType.input]

    @property
    def dragging_pos(self) -> QPointF:
        """
        Dragging pos

        Returns
        -------
        value : QPointF
        """
        return self._dragging_pos

    @dragging_pos.setter
    def dragging_position(self, pos: QPointF):
        self._dragging_pos = QPointF(pos)

    def entry_bounding_rect(self, *, addon=0.0) -> QRectF:
        """
        Entry bounding rect

        Returns
        -------
        value : QRectF
        """
        return QRectF(0 - addon, 0 - addon, self._entry_width + 2 * addon,
                      self._entry_height + 2 * addon)

    @property
    def bounding_rect(self) -> QRectF:
        """
        Bounding rect

        Returns
        -------
        value : QRectF
        """
        addon = 4 * self._style.connection_point_diameter
        return QRectF(0 - addon, 0 - addon, self._width + 2 * addon,
                      self._height + 2 * addon)

    def recalculate_size(self, font: QFont = None):
        """
        If font is unspecified,
            Updates size unconditionally
        Otherwise,
            Updates size if the QFontMetrics is changed
        """
        if font is not None:
            font_metrics = QFontMetrics(font)
            bold_font = QFont(font)
            bold_font.setBold(True)
            bold_font_metrics = QFontMetrics(bold_font)
            if self._bold_font_metrics == bold_font_metrics:
                return

            self._font_metrics = font_metrics
            self._bold_font_metrics = bold_font_metrics

        self._entry_height = self._font_metrics.height()

        max_num_of_entries = max((self.num_sinks, self.num_sources))
        step = self._entry_height + self._spacing
        height = step * max_num_of_entries

        widget = self._model.embedded_widget()
        if widget:
            height = max((height, widget.height()))

        height += self.caption_height
        self._input_port_width = self.port_width(PortType.input)
        self._output_port_width = self.port_width(PortType.output)
        width = self._input_port_width + self._output_port_width + 2 * self._spacing

        if widget:
            width += widget.width()

        width = max((width, self.caption_width))

        if self._model.validation_state() != NodeValidationState.valid:
            width = max((width, self.validation_width))
            height += self.validation_height + self._spacing

        self._width = width
        self._height = height

    def port_scene_position(self,
                            port_type: PortType,
                            index: int,
                            t: QTransform = None) -> QPointF:
        """
        Port scene position

        Parameters
        ----------
        port_type : PortType
        index : int
        t : QTransform

        Returns
        -------
        value : QPointF
        """
        if t is None:
            t = QTransform()

        step = self._entry_height + self._spacing
        total_height = float(self.caption_height) + step * index
        # TODO_UPSTREAM: why?
        total_height += step / 2.0

        if port_type == PortType.output:
            x = self._width + self._style.connection_point_diameter
            result = QPointF(x, total_height)
        elif port_type == PortType.input:
            x = -float(self._style.connection_point_diameter)
            result = QPointF(x, total_height)
        else:
            raise ValueError(port_type)

        return t.map(result)

    def check_hit_scene_point(self, port_type: PortType, scene_point: QPointF,
                              scene_transform: QTransform) -> Port:
        """
        Check hit scene point

        Parameters
        ----------
        port_type : PortType
        scene_point : QPointF
        scene_transform : QTransform

        Returns
        -------
        value : Port
        """
        if port_type == PortType.none:
            return None

        tolerance = 2.0 * self._style.connection_point_diameter
        for idx, port in self._node.state[port_type].items():
            pos = port.get_mapped_scene_position(scene_transform) - scene_point
            distance = math.sqrt(QPointF.dotProduct(pos, pos))
            if distance < tolerance:
                return port

    @property
    def resize_rect(self) -> QRect:
        """
        Resize rect

        Returns
        -------
        value : QRect
        """
        rect_size = 7
        return QRect(self._width - rect_size, self._height - rect_size,
                     rect_size, rect_size)

    @property
    def widget_position(self) -> QPointF:
        """
        Returns the position of a widget on the Node surface

        Returns
        -------
        value : QPointF
        """
        widget = self._model.embedded_widget()
        if not widget:
            return QPointF()

        if widget.sizePolicy().verticalPolicy() & QSizePolicy.ExpandFlag:
            # If the widget wants to use as much vertical space as possible,
            # place it immediately after the caption.
            return QPointF(self._spacing + self.port_width(PortType.input),
                           self.caption_height)

        if self._model.validation_state() != NodeValidationState.valid:
            return QPointF(
                self._spacing + self.port_width(PortType.input),
                (self.caption_height + self._height - self.validation_height -
                 self._spacing - widget.height()) / 2.0,
            )

        return QPointF(self._spacing + self.port_width(PortType.input),
                       (self.caption_height + self._height - widget.height()) /
                       2.0)

    def equivalent_widget_height(self) -> int:
        '''
        The maximum height a widget can be without causing the node to grow.

        Returns
        -------
        value : int
        '''
        base_height = self.height - self.caption_height

        if self._model.validation_state() != NodeValidationState.valid:
            return (base_height + self.validation_height)

        return base_height

    @property
    def validation_height(self) -> int:
        """
        Validation height

        Returns
        -------
        value : int
        """
        msg = self._model.validation_message()
        return self._bold_font_metrics.boundingRect(msg).height()

    @property
    def validation_width(self) -> int:
        """
        Validation width

        Returns
        -------
        value : int
        """
        msg = self._model.validation_message()
        return self._bold_font_metrics.boundingRect(msg).width()

    @staticmethod
    def calculate_node_position_between_node_ports(
            target_port_index: int, target_port: PortType,
            target_node: NodeBase, source_port_index: int,
            source_port: PortType, source_node: NodeBase,
            new_node: NodeBase) -> QPointF:
        """
        calculate node position between node ports

        Calculating the nodes position in the scene. It'll be positioned half
        way between the two ports that it "connects".  The first line
        calculates the halfway point between the ports (node position + port
        position on the node for both nodes averaged).  The second line offsets
        self coordinate with the size of the new node, so that the new nodes
        center falls on the originally calculated coordinate, instead of it's
        upper left corner.

        Parameters
        ----------
        target_port_index : int
        target_port : PortType
        target_node : Node
        source_port_index : int
        source_port : PortType
        source_node : Node
        new_node : Node

        Returns
        -------
        value : QPointF
        """
        converter_node_pos = (source_node.graphics_object.pos() +
                              source_node.geometry.port_scene_position(
                                  source_port, source_port_index) +
                              target_node.graphics_object.pos() +
                              target_node.geometry.port_scene_position(
                                  target_port, target_port_index)) / 2.0
        converter_node_pos.setX(converter_node_pos.x() -
                                new_node.geometry.width / 2.0)
        converter_node_pos.setY(converter_node_pos.y() -
                                new_node.geometry.height / 2.0)
        return converter_node_pos

    @property
    def caption_height(self) -> int:
        """
        Caption height

        Returns
        -------
        value : int
        """
        if not self._model.caption_visible:
            return 0
        name = self._model.caption
        return self._bold_font_metrics.boundingRect(name).height()

    @property
    def caption_width(self) -> int:
        """
        Caption width

        Returns
        -------
        value : int
        """
        if not self._model.caption_visible:
            return 0
        name = self._model.caption
        return self._bold_font_metrics.boundingRect(name).width()

    def port_width(self, port_type: PortType) -> int:
        """
        Port width

        Parameters
        ----------
        port_type : PortType

        Returns
        -------
        value : int
        """
        names = [port.display_text for port in self._node[port_type].values()]
        if not names:
            return 0

        return max(
            self._font_metrics.horizontalAdvance(name) for name in names)

    @property
    def size(self):
        """
        Get the node size

        Parameters
        ----------
        node : Node

        Returns
        -------
        value : QSizeF
        """
        return QSizeF(self.width, self.height)