diff --git a/NodeGraphQt/nodes/backdrop_node.py b/NodeGraphQt/nodes/backdrop_node.py index 1aa9d6db..9e25715b 100644 --- a/NodeGraphQt/nodes/backdrop_node.py +++ b/NodeGraphQt/nodes/backdrop_node.py @@ -1,141 +1,343 @@ #!/usr/bin/python -from NodeGraphQt.base.node import NodeObject -from NodeGraphQt.constants import NodePropWidgetEnum -from NodeGraphQt.qgraphics.node_backdrop import BackdropNodeItem +from Qt import QtGui, QtCore, QtWidgets +from NodeGraphQt.constants import Z_VAL_BACKDROP, NodeEnum +from NodeGraphQt.qgraphics.node_abstract import AbstractNodeItem +from NodeGraphQt.qgraphics.pipe import PipeItem +from NodeGraphQt.qgraphics.port import PortItem -class BackdropNode(NodeObject): + +class BackdropSizer(QtWidgets.QGraphicsItem): """ - The ``NodeGraphQt.BackdropNode`` class allows other node object to be - nested inside, it's mainly good for grouping nodes together. + Sizer item for resizing a backdrop item. - .. inheritance-diagram:: NodeGraphQt.BackdropNode + Args: + parent (BackdropNodeItem): the parent node item. + size (float): sizer size. + """ - .. image:: ../_images/backdrop.png - :width: 250px + def __init__(self, parent=None, size=6.0): + super(BackdropSizer, self).__init__(parent) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) + self.setFlag(QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges, True) + self.setCursor(QtGui.QCursor(QtCore.Qt.SizeFDiagCursor)) + self.setToolTip('double-click auto resize') + self._size = size - - - """ + @property + def size(self): + return self._size - NODE_NAME = 'Backdrop' + def set_pos(self, x, y): + x -= self._size + y -= self._size + self.setPos(x, y) - def __init__(self, qgraphics_views=None): - super(BackdropNode, self).__init__(qgraphics_views or BackdropNodeItem) - # override base default color. - self.model.color = (5, 129, 138, 255) - self.create_property('backdrop_text', '', - widget_type=NodePropWidgetEnum.QTEXT_EDIT.value, - tab='Backdrop') + def boundingRect(self): + return QtCore.QRectF(0.5, 0.5, self._size, self._size) - def on_backdrop_updated(self, update_prop, value=None): - """ - Slot triggered by the "on_backdrop_updated" signal from - the node graph. + def itemChange(self, change, value): + if change == QtWidgets.QGraphicsItem.ItemPositionChange: + item = self.parentItem() + mx, my = item.minimum_size + x = mx if value.x() < mx else value.x() + y = my if value.y() < my else value.y() + value = QtCore.QPointF(x, y) + item.on_sizer_pos_changed(value) + return value + elif change == QtWidgets.QGraphicsItem.ItemSceneHasChanged: + # Force re-render when scene changes + self.update() + return super(BackdropSizer, self).itemChange(change, value) - Args: - update_prop (str): update property type. - value (object): update value (optional) - """ - if update_prop == 'sizer_mouse_release': - self.graph.begin_undo('resized "{}"'.format(self.name())) - self.set_property('width', value['width']) - self.set_property('height', value['height']) - self.set_pos(*value['pos']) - self.graph.end_undo() - elif update_prop == 'sizer_double_clicked': - self.graph.begin_undo('"{}" auto resize'.format(self.name())) - self.set_property('width', value['width']) - self.set_property('height', value['height']) - self.set_pos(*value['pos']) - self.graph.end_undo() - - def auto_size(self): - """ - Auto resize the backdrop node to fit around the intersecting nodes. - """ - self.graph.begin_undo('"{}" auto resize'.format(self.name())) - size = self.view.calc_backdrop_size() - self.set_property('width', size['width']) - self.set_property('height', size['height']) - self.set_pos(*size['pos']) - self.graph.end_undo() - - def wrap_nodes(self, nodes): - """ - Set the backdrop size to fit around specified nodes. + def mouseDoubleClickEvent(self, event): + item = self.parentItem() + item.on_sizer_double_clicked() + super(BackdropSizer, self).mouseDoubleClickEvent(event) - Args: - nodes (list[NodeGraphQt.NodeObject]): list of nodes. - """ - if not nodes: - return - self.graph.begin_undo('"{}" wrap nodes'.format(self.name())) - size = self.view.calc_backdrop_size([n.view for n in nodes]) - self.set_property('width', size['width']) - self.set_property('height', size['height']) - self.set_pos(*size['pos']) - self.graph.end_undo() - - def nodes(self): - """ - Returns nodes wrapped within the backdrop node. + def mousePressEvent(self, event): + self.__prev_xy = (self.pos().x(), self.pos().y()) + super(BackdropSizer, self).mousePressEvent(event) - Returns: - list[NodeGraphQt.BaseNode]: list of node under the backdrop. - """ - node_ids = [n.id for n in self.view.get_nodes()] - return [self.graph.get_node_by_id(nid) for nid in node_ids] + def mouseReleaseEvent(self, event): + current_xy = (self.pos().x(), self.pos().y()) + if current_xy != self.__prev_xy: + item = self.parentItem() + item.on_sizer_pos_mouse_release() + del self.__prev_xy + super(BackdropSizer, self).mouseReleaseEvent(event) - def set_text(self, text=''): + def paint(self, painter, option, widget): """ - Sets the text to be displayed in the backdrop node. + Draws the backdrop sizer in the bottom right corner. Args: - text (str): text string. + painter (QtGui.QPainter): painter used for drawing the item. + option (QtGui.QStyleOptionGraphicsItem): + used to describe the parameters needed to draw. + widget (QtWidgets.QWidget): not used. """ - self.set_property('backdrop_text', text) + painter.save() + painter.setRenderHint(QtGui.QPainter.Antialiasing, True) - def text(self): - """ - Returns the text on the backdrop node. + margin = 1.0 + rect = self.boundingRect() + rect = QtCore.QRectF(rect.left() + margin, + rect.top() + margin, + rect.width() - (margin * 2), + rect.height() - (margin * 2)) - Returns: - str: text string. - """ - return self.get_property('backdrop_text') + item = self.parentItem() + if item and item.selected: + color = QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value) + else: + color = QtGui.QColor(*item.color) + color = color.darker(110) + path = QtGui.QPainterPath() + path.moveTo(rect.topRight()) + path.lineTo(rect.bottomRight()) + path.lineTo(rect.bottomLeft()) + painter.setBrush(color) + painter.setPen(QtCore.Qt.NoPen) + painter.fillPath(path, painter.brush()) + + painter.restore() + + +class BackdropNodeItem(AbstractNodeItem): + """ + Base Backdrop item. + + Args: + name (str): name displayed on the node. + text (str): backdrop text. + parent (QtWidgets.QGraphicsItem): parent item. + """ + + def __init__(self, name='backdrop', text='', parent=None): + super(BackdropNodeItem, self).__init__(name, parent) + self.setZValue(Z_VAL_BACKDROP) + self._properties['backdrop_text'] = text + self._min_size = 80, 80 + self._sizer = BackdropSizer(self, 26.0) + self._sizer.set_pos(*self._min_size) + self._nodes = [self] + self._last_transform = None + + def _combined_rect(self, nodes): + group = self.scene().createItemGroup(nodes) + rect = group.boundingRect() + self.scene().destroyItemGroup(group) + return rect + + def itemChange(self, change, value): + # Handle view transform changes to trigger re-rendering + if change == QtWidgets.QGraphicsItem.ItemSceneHasChanged: + self.update() + elif change == QtWidgets.QGraphicsItem.ItemTransformHasChanged: + self.update() + return super(BackdropNodeItem, self).itemChange(change, value) + + def mouseDoubleClickEvent(self, event): + viewer = self.viewer() + if viewer: + viewer.node_double_clicked.emit(self.id) + super(BackdropNodeItem, self).mouseDoubleClickEvent(event) - def set_size(self, width, height): + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + pos = event.scenePos() + rect = QtCore.QRectF(pos.x() - 5, pos.y() - 5, 10, 10) + item = self.scene().items(rect)[0] + + if isinstance(item, (PortItem, PipeItem)): + self.setFlag(self.ItemIsMovable, False) + return + if self.selected: + return + + viewer = self.viewer() + [n.setSelected(False) for n in viewer.selected_nodes()] + + self._nodes += self.get_nodes(False) + [n.setSelected(True) for n in self._nodes] + + def mouseReleaseEvent(self, event): + super(BackdropNodeItem, self).mouseReleaseEvent(event) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) + [n.setSelected(True) for n in self._nodes] + self._nodes = [self] + + def on_sizer_pos_changed(self, pos): + self._width = pos.x() + self._sizer.size + self._height = pos.y() + self._sizer.size + # Force immediate update when size changes + self.update() + + def on_sizer_pos_mouse_release(self): + size = { + 'pos': self.xy_pos, + 'width': self._width, + 'height': self._height} + self.viewer().node_backdrop_updated.emit( + self.id, 'sizer_mouse_release', size) + # Force update after resize completion + self.update() + + def on_sizer_double_clicked(self): + size = self.calc_backdrop_size() + self.viewer().node_backdrop_updated.emit( + self.id, 'sizer_double_clicked', size) + # Force update after auto-resize + self.update() + + def paint(self, painter, option, widget): """ - Sets the backdrop size. + Draws the backdrop rect. Args: - width (float): backdrop width size. - height (float): backdrop height size. + painter (QtGui.QPainter): painter used for drawing the item. + option (QtGui.QStyleOptionGraphicsItem): + used to describe the parameters needed to draw. + widget (QtWidgets.QWidget): not used. """ - if self.graph: - self.graph.begin_undo('backdrop size') - self.set_property('width', width) - self.set_property('height', height) - self.graph.end_undo() - return - self.view.width, self.view.height = width, height - self.model.width, self.model.height = width, height + painter.save() + painter.setRenderHint(QtGui.QPainter.Antialiasing, True) + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(QtCore.Qt.NoBrush) - def size(self): - """ - Returns the current size of the node. + # Check if view transform has changed and force update if needed + if self.scene() and self.scene().views(): + current_transform = self.scene().views()[0].transform() + if self._last_transform != current_transform: + self._last_transform = current_transform + self.prepareGeometryChange() - Returns: - tuple: node width, height - """ - self.model.width = self.view.width - self.model.height = self.view.height - return self.model.width, self.model.height + margin = 1.0 + rect = self.boundingRect() + rect = QtCore.QRectF(rect.left() + margin, + rect.top() + margin, + rect.width() - (margin * 2), + rect.height() - (margin * 2)) + + radius = 2.6 + color = (self.color[0], self.color[1], self.color[2], 50) + painter.setBrush(QtGui.QColor(*color)) + painter.setPen(QtCore.Qt.NoPen) + painter.drawRoundedRect(rect, radius, radius) + + top_rect = QtCore.QRectF(rect.x(), rect.y(), rect.width(), 26.0) + painter.setBrush(QtGui.QBrush(QtGui.QColor(*self.color))) + painter.setPen(QtCore.Qt.NoPen) + painter.drawRoundedRect(top_rect, radius, radius) + for pos in [top_rect.left(), top_rect.right() - 5.0]: + painter.drawRect( + QtCore.QRectF(pos, top_rect.bottom() - 5.0, 5.0, 5.0)) + + if self.backdrop_text: + painter.setPen(QtGui.QColor(*self.text_color)) + txt_rect = QtCore.QRectF( + top_rect.x() + 5.0, top_rect.height() + 3.0, + rect.width() - 5.0, rect.height()) + painter.setPen(QtGui.QColor(*self.text_color)) + painter.drawText(txt_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.TextWordWrap, + self.backdrop_text) + + if self.selected: + sel_color = [x for x in NodeEnum.SELECTED_COLOR.value] + sel_color[-1] = 15 + painter.setBrush(QtGui.QColor(*sel_color)) + painter.setPen(QtCore.Qt.NoPen) + painter.drawRoundedRect(rect, radius, radius) + + txt_rect = QtCore.QRectF(top_rect.x(), top_rect.y(), + rect.width(), top_rect.height()) + painter.setPen(QtGui.QColor(*self.text_color)) + painter.drawText(txt_rect, QtCore.Qt.AlignCenter, self.name) + + border = 0.8 + border_color = self.color + if self.selected and NodeEnum.SELECTED_BORDER_COLOR.value: + border = 1.0 + border_color = NodeEnum.SELECTED_BORDER_COLOR.value + painter.setBrush(QtCore.Qt.NoBrush) + painter.setPen(QtGui.QPen(QtGui.QColor(*border_color), border)) + painter.drawRoundedRect(rect, radius, radius) + + painter.restore() + + def get_nodes(self, inc_intersects=False): + mode = {True: QtCore.Qt.IntersectsItemShape, + False: QtCore.Qt.ContainsItemShape} + nodes = [] + if self.scene(): + polygon = self.mapToScene(self.boundingRect()) + rect = polygon.boundingRect() + items = self.scene().items(rect, mode=mode[inc_intersects]) + for item in items: + if item == self or item == self._sizer: + continue + if isinstance(item, AbstractNodeItem): + nodes.append(item) + return nodes + + def calc_backdrop_size(self, nodes=None): + nodes = nodes or self.get_nodes(True) + if nodes: + nodes_rect = self._combined_rect(nodes) + else: + center = self.mapToScene(self.boundingRect().center()) + nodes_rect = QtCore.QRectF( + center.x(), center.y(), + self._min_size[0], self._min_size[1] + ) + + padding = 40 + return { + 'pos': [ + nodes_rect.x() - padding, nodes_rect.y() - padding + ], + 'width': nodes_rect.width() + (padding * 2), + 'height': nodes_rect.height() + (padding * 2) + } + + @property + def minimum_size(self): + return self._min_size + + @minimum_size.setter + def minimum_size(self, size=(50, 50)): + self._min_size = size + self.update() + + @property + def backdrop_text(self): + return self._properties['backdrop_text'] + + @backdrop_text.setter + def backdrop_text(self, text): + self._properties['backdrop_text'] = text + self.update(self.boundingRect()) + + @AbstractNodeItem.width.setter + def width(self, width=0.0): + AbstractNodeItem.width.fset(self, width) + self._sizer.set_pos(self._width, self._height) + self.update() - def inputs(self): - # required function but unused for the backdrop node. - return + @AbstractNodeItem.height.setter + def height(self, height=0.0): + AbstractNodeItem.height.fset(self, height) + self._sizer.set_pos(self._width, self._height) + self.update() - def outputs(self): - # required function but unused for the backdrop node. - return + def from_dict(self, node_dict): + super().from_dict(node_dict) + custom_props = node_dict.get('custom') or {} + for prop_name, value in custom_props.items(): + if prop_name == 'backdrop_text': + self.backdrop_text = value + # Force update after loading from dict + self.update()