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)
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)
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)
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
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()
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)
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)
def get_text_width(text) -> int: """Return the width required to render ``text``.""" fm = QFontMetrics(QFont("", 0)) return fm.boundingRect(text).width() + 5
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)