#!/usr/bin/env python3 from Qt import QtWidgets, QtGui from OdenGraphQt import BaseNode from OdenGraphQt.constants import NodePropWidgetEnum from OdenGraphQt.qgraphics.node_backdrop import BackdropNodeItem class BackdropNode(BaseNode): """ Backdrop Node: - Allows grouping or annotating other nodes by resizing a large rectangle. - Provides a custom context menu for renaming and recoloring (via on_context_menu). """ __identifier__ = 'bunny-lab.io.backdrop' NODE_NAME = 'Backdrop' def __init__(self): # Use BackdropNodeItem for the specialized QGraphicsItem. super(BackdropNode, self).__init__(qgraphics_item=BackdropNodeItem) # Default color (teal). self.model.color = (5, 129, 138, 255) # Multi-line text property for storing the backdrop text. self.create_property( 'backdrop_text', '', widget_type=NodePropWidgetEnum.QTEXT_EDIT.value, tab='Backdrop' ) # -------------------------------------------------------------------------- # Resizing / Geometry # -------------------------------------------------------------------------- def on_backdrop_updated(self, update_prop, value=None): """ Triggered when the user resizes or double-clicks the backdrop sizer handle. """ if not self.graph: return if update_prop == 'sizer_mouse_release': # User finished dragging the resize handle self.view.prepareGeometryChange() self.graph.begin_undo(f'resized "{self.name()}"') self.set_property('width', value['width']) self.set_property('height', value['height']) self.set_pos(*value['pos']) self.graph.end_undo() self.view.update() elif update_prop == 'sizer_double_clicked': # User double-clicked the resize handle (auto-resize) self.view.prepareGeometryChange() self.graph.begin_undo(f'"{self.name()}" auto resize') self.set_property('width', value['width']) self.set_property('height', value['height']) self.set_pos(*value['pos']) self.graph.end_undo() self.view.update() def auto_size(self): """ Auto-resize the backdrop to fit around intersecting nodes. """ if not self.graph: return self.view.prepareGeometryChange() self.graph.begin_undo(f'"{self.name()}" auto resize') 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() self.view.update() def wrap_nodes(self, nodes): """ Fit the backdrop around the specified nodes. """ if not self.graph or not nodes: return self.view.prepareGeometryChange() self.graph.begin_undo(f'"{self.name()}" wrap nodes') 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() self.view.update() def nodes(self): """ Return a list of nodes wrapped by this 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 set_text(self, text=''): """ Set the multi-line text in the backdrop. """ self.set_property('backdrop_text', text) def text(self): """ Return the text content in the backdrop. """ return self.get_property('backdrop_text') def set_size(self, width, height): """ Manually set the backdrop size. """ if self.graph: self.view.prepareGeometryChange() self.graph.begin_undo('backdrop size') self.set_property('width', width) self.set_property('height', height) self.graph.end_undo() self.view.update() else: self.view.width, self.view.height = width, height self.model.width, self.model.height = width, height def size(self): """ Return (width, height) of the backdrop. """ self.model.width = self.view.width self.model.height = self.view.height return self.model.width, self.model.height # No ports for a backdrop: def inputs(self): return def outputs(self): return # -------------------------------------------------------------------------- # Custom Context Menu # -------------------------------------------------------------------------- def on_context_menu(self, menu): """ Called manually by the node context menu callback in older NodeGraphQt versions. """ rename_action = menu.addAction("Set Title...") rename_action.triggered.connect(self._change_title) color_action = menu.addAction("Set Color...") color_action.triggered.connect(self._change_color) def _change_title(self): """ Prompt for a new backdrop title (header). """ new_title, ok = QtWidgets.QInputDialog.getText( None, "Backdrop Title", "Enter new backdrop title:" ) if ok and new_title: self.set_name(new_title) def _change_color(self): """ Prompt for a new backdrop color via QColorDialog. """ current_color = QtGui.QColor(*self.model.color) color = QtWidgets.QColorDialog.getColor( current_color, None, "Select Backdrop Color" ) if color.isValid(): self.model.color = (color.red(), color.green(), color.blue(), color.alpha()) self.view.update()