def paint(self, painter, option, widget): """ Draws the backdrop rect. Args: 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. """ painter.save() rect = self.boundingRect() color = (self.color[0], self.color[1], self.color[2], 50) painter.setBrush(QtGui.QColor(*color)) painter.setPen(QtCore.Qt.NoPen) painter.drawRect(rect) top_rect = QtCore.QRectF(0.0, 0.0, rect.width(), 20.0) painter.setBrush(QtGui.QColor(*self.color)) painter.setPen(QtCore.Qt.NoPen) painter.drawRect(top_rect) if self.backdrop_text: painter.setPen(QtGui.QColor(*self.text_color)) txt_rect = QtCore.QRectF(top_rect.x() + 5.0, top_rect.height() + 2.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 and NODE_SEL_COLOR: sel_color = [x for x in NODE_SEL_COLOR] sel_color[-1] = 10 painter.setBrush(QtGui.QColor(*sel_color)) painter.setPen(QtCore.Qt.NoPen) painter.drawRect(rect) txt_rect = QtCore.QRectF(top_rect.x(), top_rect.y() + 1.2, rect.width(), top_rect.height()) painter.setPen(QtGui.QColor(*self.text_color)) painter.drawText(txt_rect, QtCore.Qt.AlignCenter, self.name) path = QtGui.QPainterPath() path.addRect(rect) border_color = self.color if self.selected and NODE_SEL_BORDER_COLOR: border_color = NODE_SEL_BORDER_COLOR painter.setBrush(QtCore.Qt.NoBrush) painter.setPen(QtGui.QPen(QtGui.QColor(*border_color), 1)) painter.drawPath(path) painter.restore()
def paint(self, painter, option, widget): """ Draws the node base not the ports. Args: 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. """ painter.save() bg_border = 1.0 rect = QtCore.QRectF(0.5 - (bg_border / 2), 0.5 - (bg_border / 2), self._width + bg_border, self._height + bg_border) radius = 2 border_color = QtGui.QColor(*self.border_color) path = QtGui.QPainterPath() path.addRoundedRect(rect, radius, radius) rect = self.boundingRect() bg_color = QtGui.QColor(*self.color) painter.setBrush(bg_color) painter.setPen(QtCore.Qt.NoPen) painter.drawRoundedRect(rect, radius, radius) if self.selected and NODE_SEL_COLOR: painter.setBrush(QtGui.QColor(*NODE_SEL_COLOR)) painter.drawRoundedRect(rect, radius, radius) label_rect = QtCore.QRectF(rect.left() + (radius / 2), rect.top() + (radius / 2), self._width - (radius / 1.25), 28) path = QtGui.QPainterPath() path.addRoundedRect(label_rect, radius / 1.5, radius / 1.5) painter.setBrush(QtGui.QColor(0, 0, 0, 50)) painter.fillPath(path, painter.brush()) border_width = 0.8 if self.selected and NODE_SEL_BORDER_COLOR: border_width = 1.2 border_color = QtGui.QColor(*NODE_SEL_BORDER_COLOR) border_rect = QtCore.QRectF(rect.left() - (border_width / 2), rect.top() - (border_width / 2), rect.width() + border_width, rect.height() + border_width) pen = QtGui.QPen(border_color, border_width) pen.setCosmetic(self.viewer().get_zoom() < 0.0) path = QtGui.QPainterPath() path.addRoundedRect(border_rect, radius, radius) painter.setBrush(QtCore.Qt.NoBrush) painter.setPen(pen) painter.drawPath(path) painter.restore()
def paint(self, painter, option, widget): """ Draws the circular port. Args: 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. """ painter.save() rect = QtCore.QRectF(0.0, 0.8, self._width, self._height) path = QtGui.QPainterPath() path.addEllipse(rect) if self._hovered: color = QtGui.QColor(*PORT_HOVER_COLOR) border_color = QtGui.QColor(*PORT_HOVER_BORDER_COLOR) elif self.connected_pipes: color = QtGui.QColor(*PORT_ACTIVE_COLOR) border_color = QtGui.QColor(*PORT_ACTIVE_BORDER_COLOR) else: color = QtGui.QColor(*self.color) border_color = QtGui.QColor(*self.border_color) painter.setBrush(color) pen = QtGui.QPen(border_color, 1.5) painter.setPen(pen) painter.drawEllipse(self.boundingRect()) if self.connected_pipes and not self._hovered: painter.setBrush(border_color) w = self._width / 2.5 h = self._height / 2.5 rect = QtCore.QRectF(self.boundingRect().center().x() - w / 2, self.boundingRect().center().y() - h / 2, w, h) painter.drawEllipse(rect) elif self._hovered: if self.multi_connection: painter.setBrush(color) w = self._width / 1.8 h = self._height / 1.8 else: painter.setBrush(border_color) w = self._width / 3.5 h = self._height / 3.5 rect = QtCore.QRectF(self.boundingRect().center().x() - w / 2, self.boundingRect().center().y() - h / 2, w, h) painter.drawEllipse(rect) painter.restore()
def draw_path(self, start_port, end_port, cursor_pos=None): """ Draws the path between ports. Args: start_port (PortItem): port used to draw the starting point. end_port (PortItem): port used to draw the end point. cursor_pos (QtCore.QPointF): cursor position if specified this will be the draw end point. """ if not start_port: return offset = (start_port.boundingRect().width() / 2) pos1 = start_port.scenePos() pos1.setX(pos1.x() + offset) pos1.setY(pos1.y() + offset) if cursor_pos: pos2 = cursor_pos elif end_port: offset = start_port.boundingRect().width() / 2 pos2 = end_port.scenePos() pos2.setX(pos2.x() + offset) pos2.setY(pos2.y() + offset) else: return line = QtCore.QLineF(pos1, pos2) path = QtGui.QPainterPath() path.moveTo(line.x1(), line.y1()) if self.viewer_pipe_layout() == PIPE_LAYOUT_STRAIGHT: path.lineTo(pos2) self.setPath(path) return ctr_offset_x1, ctr_offset_x2 = pos1.x(), pos2.x() tangent = ctr_offset_x1 - ctr_offset_x2 tangent = (tangent * -1) if tangent < 0 else tangent max_width = start_port.node.boundingRect().width() / 2 tangent = max_width if tangent > max_width else tangent if start_port.port_type == IN_PORT: ctr_offset_x1 -= tangent ctr_offset_x2 += tangent else: ctr_offset_x1 += tangent ctr_offset_x2 -= tangent ctr_point1 = QtCore.QPointF(ctr_offset_x1, pos1.y()) ctr_point2 = QtCore.QPointF(ctr_offset_x2, pos2.y()) path.cubicTo(ctr_point1, ctr_point2, pos2) self.setPath(path)
def _draw_grid(self, painter, rect, pen, grid_size): lines = [] left = int(rect.left()) - (int(rect.left()) % grid_size) top = int(rect.top()) - (int(rect.top()) % grid_size) x = left while x < rect.right(): x += grid_size lines.append(QtCore.QLineF(x, rect.top(), x, rect.bottom())) y = top while y < rect.bottom(): y += grid_size lines.append(QtCore.QLineF(rect.left(), y, rect.right(), y)) painter.setPen(pen) painter.drawLines(lines)
def __init__(self, input_port=None, output_port=None): super(Pipe, self).__init__() self.setZValue(Z_VAL_PIPE) self.setAcceptHoverEvents(True) self._color = PIPE_DEFAULT_COLOR self._style = PIPE_STYLE_DEFAULT self._active = False self._highlight = False self._input_port = input_port self._output_port = output_port size = 6.0 self._arrow = QtGui.QPolygonF() self._arrow.append(QtCore.QPointF(-size, size)) self._arrow.append(QtCore.QPointF(0.0, -size * 1.5)) self._arrow.append(QtCore.QPointF(size, size))
def paint(self, painter, option, index): """ Args: painter (QtGui.QPainter): option (QtGui.QStyleOptionViewItem): index (QtCore.QModelIndex): """ painter.save() painter.setRenderHint(QtGui.QPainter.Antialiasing, False) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(option.palette.midlight()) painter.drawRect(option.rect) if option.state & QtWidgets.QStyle.State_Selected: bdr_clr = option.palette.highlight().color() painter.setPen(QtGui.QPen(bdr_clr, 1.5)) else: bdr_clr = option.palette.alternateBase().color() painter.setPen(QtGui.QPen(bdr_clr, 1)) painter.setBrush(QtCore.Qt.NoBrush) painter.drawRect(QtCore.QRect(option.rect.x() + 1, option.rect.y() + 1, option.rect.width() - 2, option.rect.height() - 2)) painter.restore()
class TabSearchWidget(QtWidgets.QLineEdit): search_submitted = QtCore.Signal(str) def __init__(self, parent=None, node_dict=None): super(TabSearchWidget, self).__init__(parent) self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 0) self.setStyleSheet(STYLE_TABSEARCH) self.setMinimumSize(200, 22) self.setTextMargins(2, 0, 2, 0) self.hide() self._node_dict = node_dict or {} node_names = sorted(self._node_dict.keys()) self._model = QtCore.QStringListModel(node_names, self) self._completer = TabSearchCompleter() self._completer.setModel(self._model) self.setCompleter(self._completer) popup = self._completer.popup() popup.setStyleSheet(STYLE_TABSEARCH_LIST) popup.clicked.connect(self._on_search_submitted) self.returnPressed.connect(self._on_search_submitted) def __repr__(self): return '<{} at {}>'.format(self.__class__.__name__, hex(id(self))) def _on_search_submitted(self, index=0): node_type = self._node_dict.get(self.text()) if node_type: self.search_submitted.emit(node_type) self.close() self.parentWidget().clearFocus() def showEvent(self, event): super(TabSearchWidget, self).showEvent(event) self.setSelection(0, len(self.text())) self.setFocus() if not self.text(): self.completer().popup().show() self.completer().complete() def mousePressEvent(self, event): if not self.text(): self.completer().complete() def set_nodes(self, node_dict=None): self._node_dict = {} for name, node_types in node_dict.items(): if len(node_types) == 1: self._node_dict[name] = node_types[0] continue for node_id in node_types: self._node_dict['{} ({})'.format(name, node_id)] = node_id node_names = sorted(self._node_dict.keys()) self._model.setStringList(node_names) self._completer.setModel(self._model)
def updateModel(self): if not self._using_orig_model: self._filter_model.setSourceModel(self._source_model) pattern = QtCore.QRegExp(self._local_completion_prefix, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString) self._filter_model.setFilterRegExp(pattern)
def splitPath(self, path): self._local_completion_prefix = path self.updateModel() if self._filter_model.rowCount() == 0: self._using_orig_model = False self._filter_model.setSourceModel(QtCore.QStringListModel([path])) return [path] return []
def itemChange(self, change, value): if change == self.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 return super(BackdropSizer, self).itemChange(change, value)
def paint(self, painter, option, widget): """ Draws the slicer pipe. Args: 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. """ color = QtGui.QColor(*PIPE_SLICER_COLOR) p1 = self.path().pointAtPercent(0) p2 = self.path().pointAtPercent(1) size = 6.0 offset = size / 2 painter.save() painter.setRenderHint(painter.Antialiasing, True) font = painter.font() font.setPointSize(12) painter.setFont(font) text = 'slice' text_x = painter.fontMetrics().width(text) / 2 text_y = painter.fontMetrics().height() / 1.5 text_pos = QtCore.QPointF(p1.x() - text_x, p1.y() - text_y) text_color = QtGui.QColor(*PIPE_SLICER_COLOR) text_color.setAlpha(80) painter.setPen(QtGui.QPen(text_color, 1.5, QtCore.Qt.SolidLine)) painter.drawText(text_pos, text) painter.setPen(QtGui.QPen(color, 1.5, QtCore.Qt.DashLine)) painter.drawPath(self.path()) painter.setPen(QtGui.QPen(color, 1.5, QtCore.Qt.SolidLine)) painter.setBrush(color) rect = QtCore.QRectF(p1.x() - offset, p1.y() - offset, size, size) painter.drawEllipse(rect) rect = QtCore.QRectF(p2.x() - offset, p2.y() - offset, size, size) painter.drawEllipse(rect) painter.restore()
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, Pipe)): 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 add_command(self, name, func=None, shortcut=None): """ Adds a command to the menu. Args: name (str): command name. func (function): command function. shortcut (str): function shotcut key. Returns: NodeGraphQt.MenuCommand: the appended command. """ action = QtWidgets.QAction(name, self.__viewer) if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): action.setShortcutVisibleInContextMenu(True) if shortcut: action.setShortcut(shortcut) if func: action.triggered.connect(func) qaction = self.qmenu.addAction(action) return MenuCommand(self.__viewer, qaction)
def __init__(self, parent=None, node_dict=None): super(TabSearchWidget, self).__init__(parent) self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 0) self.setStyleSheet(STYLE_TABSEARCH) self.setMinimumSize(200, 22) self.setTextMargins(2, 0, 2, 0) self.hide() self._node_dict = node_dict or {} node_names = sorted(self._node_dict.keys()) self._model = QtCore.QStringListModel(node_names, self) self._completer = TabSearchCompleter() self._completer.setModel(self._model) self.setCompleter(self._completer) popup = self._completer.popup() popup.setStyleSheet(STYLE_TABSEARCH_LIST) popup.clicked.connect(self._on_search_submitted) self.returnPressed.connect(self._on_search_submitted)
def boundingRect(self): return QtCore.QRectF(0.0, 0.0, self._width, self._height)
def boundingRect(self): return QtCore.QRectF(0.5, 0.5, self._size, self._size)
def paint(self, painter, option, widget): """ Draws the overlay disabled X item on top of a node item. Args: 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. """ painter.save() margin = 20 rect = self.boundingRect() dis_rect = QtCore.QRectF(rect.left() - (margin / 2), rect.top() - (margin / 2), rect.width() + margin, rect.height() + margin) pen = QtGui.QPen(QtGui.QColor(*self.color), 8) pen.setCapStyle(QtCore.Qt.RoundCap) painter.setPen(pen) painter.drawLine(dis_rect.topLeft(), dis_rect.bottomRight()) painter.drawLine(dis_rect.topRight(), dis_rect.bottomLeft()) bg_color = QtGui.QColor(*self.color) bg_color.setAlpha(100) bg_margin = -0.5 bg_rect = QtCore.QRectF(dis_rect.left() - (bg_margin / 2), dis_rect.top() - (bg_margin / 2), dis_rect.width() + bg_margin, dis_rect.height() + bg_margin) painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, 0))) painter.setBrush(bg_color) painter.drawRoundedRect(bg_rect, 5, 5) pen = QtGui.QPen(QtGui.QColor(155, 0, 0, 255), 0.7) painter.setPen(pen) painter.drawLine(dis_rect.topLeft(), dis_rect.bottomRight()) painter.drawLine(dis_rect.topRight(), dis_rect.bottomLeft()) point_size = 4.0 point_pos = (dis_rect.topLeft(), dis_rect.topRight(), dis_rect.bottomLeft(), dis_rect.bottomRight()) painter.setBrush(QtGui.QColor(255, 0, 0, 255)) for p in point_pos: p.setX(p.x() - (point_size / 2)) p.setY(p.y() - (point_size / 2)) point_rect = QtCore.QRectF(p, QtCore.QSizeF(point_size, point_size)) painter.drawEllipse(point_rect) if self.text: font = painter.font() font.setPointSize(10) painter.setFont(font) font_metrics = QtGui.QFontMetrics(font) font_width = font_metrics.width(self.text) font_height = font_metrics.height() txt_w = font_width * 1.25 txt_h = font_height * 2.25 text_bg_rect = QtCore.QRectF((rect.width() / 2) - (txt_w / 2), (rect.height() / 2) - (txt_h / 2), txt_w, txt_h) painter.setPen(QtGui.QPen(QtGui.QColor(255, 0, 0), 0.5)) painter.setBrush(QtGui.QColor(*self.color)) painter.drawRoundedRect(text_bg_rect, 2, 2) text_rect = QtCore.QRectF((rect.width() / 2) - (font_width / 2), (rect.height() / 2) - (font_height / 2), txt_w * 2, font_height * 2) painter.setPen(QtGui.QPen(QtGui.QColor(255, 0, 0), 1)) painter.drawText(text_rect, self.text) painter.restore()
def setModel(self, model): self._source_model = model self._filter_model = QtCore.QSortFilterProxyModel(self) self._filter_model.setSourceModel(self._source_model) super(TabSearchCompleter, self).setModel(self._filter_model) self._using_orig_model = True
def setup_context_menu(graph): """ Sets up the node graphs context menu with some basic menus and commands. Args: graph (NodeGraphQt.NodeGraph): node graph. """ root_menu = graph.context_menu() file_menu = root_menu.add_menu('&File') edit_menu = root_menu.add_menu('&Edit') # create "File" menu. file_menu.add_command('Open...', lambda: _open_session(graph), QtGui.QKeySequence.Open) file_menu.add_command('Save...', lambda: _save_session(graph), QtGui.QKeySequence.Save) file_menu.add_command('Save As...', lambda: _save_session_as(graph), 'Ctrl+Shift+s') file_menu.add_command('Clear', lambda: _clear_session(graph)) file_menu.add_separator() file_menu.add_command('Zoom In', lambda: _zoom_in(graph), '=') file_menu.add_command('Zoom Out', lambda: _zoom_out(graph), '-') file_menu.add_command('Reset Zoom', graph.reset_zoom, 'h') # create "Edit" menu. undo_actn = graph.undo_stack().createUndoAction(graph.viewer(), '&Undo') if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): undo_actn.setShortcutVisibleInContextMenu(True) undo_actn.setShortcuts(QtGui.QKeySequence.Undo) edit_menu.qmenu.addAction(undo_actn) redo_actn = graph.undo_stack().createRedoAction(graph.viewer(), '&Redo') if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): redo_actn.setShortcutVisibleInContextMenu(True) redo_actn.setShortcuts(QtGui.QKeySequence.Redo) edit_menu.qmenu.addAction(redo_actn) edit_menu.add_separator() edit_menu.add_command('Clear Undo History', lambda: _clear_undo(graph)) edit_menu.add_separator() edit_menu.add_command('Copy', graph.copy_nodes, QtGui.QKeySequence.Copy) edit_menu.add_command('Paste', graph.paste_nodes, QtGui.QKeySequence.Paste) edit_menu.add_command('Delete', lambda: graph.delete_nodes(graph.selected_nodes()), QtGui.QKeySequence.Delete) edit_menu.add_separator() edit_menu.add_command('Select all', graph.select_all, 'Ctrl+A') edit_menu.add_command('Deselect all', graph.clear_selection, 'Ctrl+Shift+A') edit_menu.add_command('Enable/Disable', lambda: graph.disable_nodes(graph.selected_nodes()), 'd') edit_menu.add_command( 'Duplicate', lambda: graph.duplicate_nodes(graph.selected_nodes()), 'Alt+c') edit_menu.add_command('Center Selection', graph.fit_to_selection, 'f') edit_menu.add_separator()
class NodeGraph(QtCore.QObject): """ base node graph controller. """ #: signal emits the node object when a node is created in the node graph. node_created = QtCore.Signal(NodeObject) #: signal emits a list of node ids from the deleted nodes. nodes_deleted = QtCore.Signal(list) #: signal emits the node object when selected in the node graph. node_selected = QtCore.Signal(NodeObject) #: signal triggered when a node is double clicked and emits the node. node_double_clicked = QtCore.Signal(NodeObject) #: signal for when a node has been connected emits (source port, target port). port_connected = QtCore.Signal(Port, Port) #: signal for when a node property has changed emits (node, property name, property value). property_changed = QtCore.Signal(NodeObject, str, object) #: signal for when drop data has been added to the graph. data_dropped = QtCore.Signal(QtCore.QMimeData, QtCore.QPoint) def __init__(self, parent=None): super(NodeGraph, self).__init__(parent) self.setObjectName('NodeGraphQt') self._model = NodeGraphModel() self._viewer = NodeViewer(parent) self._node_factory = NodeFactory() self._undo_stack = QtWidgets.QUndoStack(self) tab = QtWidgets.QAction('Search Nodes', self) tab.setShortcut('tab') tab.triggered.connect(self._toggle_tab_search) self._viewer.addAction(tab) self._wire_signals() def __repr__(self): return '<{} object at {}>'.format(self.__class__.__name__, hex(id(self))) def _wire_signals(self): # internal signals. self._viewer.search_triggered.connect(self._on_search_triggered) self._viewer.connection_sliced.connect(self._on_connection_sliced) self._viewer.connection_changed.connect(self._on_connection_changed) self._viewer.moved_nodes.connect(self._on_nodes_moved) self._viewer.node_double_clicked.connect(self._on_node_double_clicked) # pass through signals. self._viewer.node_selected.connect(self._on_node_selected) self._viewer.data_dropped.connect(self._on_node_data_dropped) def _toggle_tab_search(self): """ toggle the tab search widget. """ self._viewer.tab_search_set_nodes(self._node_factory.names) self._viewer.tab_search_toggle() def _on_property_bin_changed(self, node_id, prop_name, prop_value): """ called when a property widget has changed in a properties bin. (emits the node object, property name, property value) Args: node_id (str): node id. prop_name (str): node property name. prop_value (object): python object. """ node = self.get_node_by_id(node_id) # prevent signals from causing a infinite loop. if node.get_property(prop_name) != prop_value: node.set_property(prop_name, prop_value) def _on_node_double_clicked(self, node_id): """ called when a node in the viewer is double click. (emits the node object when the node is clicked) Args: node_id (str): node id emitted by the viewer. """ node = self.get_node_by_id(node_id) self.node_double_clicked.emit(node) def _on_node_selected(self, node_id): """ called when a node in the viewer is selected on left click. (emits the node object when the node is clicked) Args: node_id (str): node id emitted by the viewer. """ node = self.get_node_by_id(node_id) self.node_selected.emit(node) def _on_node_data_dropped(self, data, pos): """ called when data has been dropped on the viewer. Args: data (QtCore.QMimeData): mime data. pos (QtCore.QPoint): scene position relative to the drop. """ # don't emit signal for internal widget drops. if data.hasFormat('text/plain'): if data.text().startswith('<${}>:'.format(DRAG_DROP_ID)): node_ids = data.text()[len('<${}>:'.format(DRAG_DROP_ID)):] x, y = pos.x(), pos.y() for node_id in node_ids.split(','): self.create_node(node_id, pos=[x, y]) x += 20 y += 20 return self.data_dropped.emit(data, pos) def _on_nodes_moved(self, node_data): """ called when selected nodes in the viewer has changed position. Args: node_data (dict): {<node_view>: <previous_pos>} """ self._undo_stack.beginMacro('move nodes') for node_view, prev_pos in node_data.items(): node = self._model.nodes[node_view.id] self._undo_stack.push(NodeMovedCmd(node, node.pos(), prev_pos)) self._undo_stack.endMacro() def _on_search_triggered(self, node_type, pos): """ called when the tab search widget is triggered in the viewer. Args: node_type (str): node identifier. pos (tuple): x,y position for the node. """ self.create_node(node_type, pos=pos) def _on_connection_changed(self, disconnected, connected): """ called when a pipe connection has been changed in the viewer. Args: disconnected (list[list[widgets.port.PortItem]): pair list of port view items. connected (list[list[widgets.port.PortItem]]): pair list of port view items. """ if not (disconnected or connected): return label = 'connect node(s)' if connected else 'disconnect node(s)' ptypes = {'in': 'inputs', 'out': 'outputs'} self._undo_stack.beginMacro(label) for p1_view, p2_view in disconnected: node1 = self._model.nodes[p1_view.node.id] node2 = self._model.nodes[p2_view.node.id] port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name] port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name] port1.disconnect_from(port2) for p1_view, p2_view in connected: node1 = self._model.nodes[p1_view.node.id] node2 = self._model.nodes[p2_view.node.id] port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name] port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name] port1.connect_to(port2) self._undo_stack.endMacro() def _on_connection_sliced(self, ports): """ slot when connection pipes have been sliced. Args: ports (list[list[widgets.port.PortItem]]): pair list of port connections (in port, out port) """ if not ports: return ptypes = {'in': 'inputs', 'out': 'outputs'} self._undo_stack.beginMacro('slice connections') for p1_view, p2_view in ports: node1 = self._model.nodes[p1_view.node.id] node2 = self._model.nodes[p2_view.node.id] port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name] port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name] port1.disconnect_from(port2) self._undo_stack.endMacro() @property def model(self): """ Returns the model used to store the node graph data. Returns: NodeGraphQt.base.model.NodeGraphModel: node graph model. """ return self._model def show(self): """ Show node graph viewer widget this is just a convenience function to :meth:`NodeGraph.viewer().show()`. """ self._viewer.show() def close(self): """ Close node graph NodeViewer widget this is just a convenience function to :meth:`NodeGraph.viewer().close()`. """ self._viewer.close() def viewer(self): """ Return the node graph viewer widget. Returns: NodeGraphQt.widgets.viewer.NodeViewer: viewer widget. """ return self._viewer def scene(self): """ Return the scene object. Returns: NodeGraphQt.widgets.scene.NodeScene: node scene. """ return self._viewer.scene() def background_color(self): """ Return the node graph background color. Returns: tuple: r, g ,b """ return self.scene().background_color def set_background_color(self, r, g, b): """ Set node graph background color. Args: r (int): red value. g (int): green value. b (int): blue value. """ self.scene().background_color = (r, g, b) def grid_color(self): """ Return the node graph grid color. Returns: tuple: r, g ,b """ return self.scene().grid_color def set_grid_color(self, r, g, b): """ Set node graph grid color. Args: r (int): red value. g (int): green value. b (int): blue value. """ self.scene().grid_color = (r, g, b) def display_grid(self, display=True): """ Display node graph background grid. Args: display: False to not draw the background grid. """ self.scene().grid = display def add_properties_bin(self, prop_bin): """ Wire up a properties bin widget to the node graph. Args: prop_bin (NodeGraphQt.PropertiesBinWidget): properties widget. """ prop_bin.property_changed.connect(self._on_property_bin_changed) def undo_stack(self): """ Returns the undo stack used in the node graph Returns: QtWidgets.QUndoStack: undo stack. """ return self._undo_stack def clear_undo_stack(self): """ Clears the undo stack. (convenience function to :meth:`NodeGraph.undo_stack().clear`) """ self._undo_stack.clear() def begin_undo(self, name): """ Start of an undo block followed by a :meth:`NodeGraph.end_undo()`. Args: name (str): name for the undo block. """ self._undo_stack.beginMacro(name) def end_undo(self): """ End of an undo block started by :meth:`NodeGraph.begin_undo()`. """ self._undo_stack.endMacro() def context_menu(self): """ Returns the node graph root context menu object. Returns: Menu: context menu object. """ return Menu(self._viewer, self._viewer.context_menu()) def acyclic(self): """ Returns true if the current node graph is acyclic. Returns: bool: true if acyclic (default: True). """ return self._model.acyclic def set_acyclic(self, mode=False): """ Enable the node graph to be a acyclic graph. (default=False) Args: mode (bool): true to enable acyclic. """ self._model.acyclic = mode self._viewer.acyclic = mode def set_pipe_layout(self, layout='curved'): """ Set node graph pipes to be drawn straight or curved by default all pipes are set curved. (default='curved') Args: layout (str): 'straight' or 'curved' """ self._viewer.set_pipe_layout(layout) def fit_to_selection(self): """ Sets the zoom level to fit selected nodes. If no nodes are selected then all nodes in the graph will be framed. """ nodes = self.selected_nodes() or self.all_nodes() if not nodes: return self._viewer.zoom_to_nodes([n.view for n in nodes]) def reset_zoom(self): """ Reset the zoom level """ self._viewer.reset_zoom() def set_zoom(self, zoom=0): """ Set the zoom factor of the Node Graph the default is 0.0 Args: zoom (float): zoom factor (max zoom out -0.9 / max zoom in 2.0) """ self._viewer.set_zoom(zoom) def get_zoom(self): """ Get the current zoom level of the node graph. Returns: float: the current zoom level. """ return self._viewer.get_zoom() def center_on(self, nodes=None): """ Center the node graph on the given nodes or all nodes by default. Args: nodes (list[NodeGraphQt.BaseNode]): a list of nodes. """ self._viewer.center_selection(nodes) def center_selection(self): """ Centers on the current selected nodes. """ nodes = self._viewer.selected_nodes() self._viewer.center_selection(nodes) def registered_nodes(self): """ Return a list of all node types that have been registered. To register a node see :meth:`NodeGraph.register_node` Returns: list[str]: list of node type identifiers. """ return sorted(self._node_factory.nodes.keys()) def register_node(self, node, alias=None): """ Register the node to the node graph vendor. Args: node (NodeGraphQt.NodeObject): node. alias (str): custom alias name for the node type. """ self._node_factory.register_node(node, alias) def create_node(self, node_type, name=None, selected=True, color=None, text_color=None, pos=None): """ Create a new node in the node graph. (To list all node types see :meth:`NodeGraph.registered_nodes`) Args: node_type (str): node instance type. name (str): set name of the node. selected (bool): set created node to be selected. color (tuple or str): node color (255, 255, 255) or '#FFFFFF'. text_color (tuple or str): node text color (255, 255, 255) or '#FFFFFF'. pos (list[int, int]): initial x, y position for the node (default: (0, 0)). Returns: NodeGraphQt.BaseNode: the created instance of the node. """ NodeCls = self._node_factory.create_node_instance(node_type) if NodeCls: node = NodeCls() node._graph = self node.model._graph_model = self.model wid_types = node.model.__dict__.pop('_TEMP_property_widget_types') prop_attrs = node.model.__dict__.pop('_TEMP_property_attrs') if self.model.get_node_common_properties(node.type_) is None: node_attrs = { node.type_: {n: { 'widget_type': wt } for n, wt in wid_types.items()} } for pname, pattrs in prop_attrs.items(): node_attrs[node.type_][pname].update(pattrs) self.model.set_node_common_properties(node_attrs) node.NODE_NAME = self.get_unique_name(name or node.NODE_NAME) node.model.name = node.NODE_NAME node.model.selected = selected def format_color(clr): if isinstance(clr, str): clr = clr.strip('#') return tuple(int(clr[i:i + 2], 16) for i in (0, 2, 4)) return clr if color: node.model.color = format_color(color) if text_color: node.model.text_color = format_color(text_color) if pos: node.model.pos = [float(pos[0]), float(pos[1])] node.update() undo_cmd = NodeAddedCmd(self, node, node.model.pos) undo_cmd.setText('create node: "{}"'.format(node.NODE_NAME)) self._undo_stack.push(undo_cmd) self.node_created.emit(node) return node raise Exception('\n\n>> Cannot find node:\t"{}"\n'.format(node_type)) def add_node(self, node, pos=None): """ Add a node into the node graph. Args: node (NodeGraphQt.BaseNode): node object. pos (list[float]): node x,y position. (optional) """ assert isinstance(node, NodeObject), 'node must be a Node instance.' wid_types = node.model.__dict__.pop('_TEMP_property_widget_types') prop_attrs = node.model.__dict__.pop('_TEMP_property_attrs') if self.model.get_node_common_properties(node.type_) is None: node_attrs = { node.type_: {n: { 'widget_type': wt } for n, wt in wid_types.items()} } for pname, pattrs in prop_attrs.items(): node_attrs[node.type_][pname].update(pattrs) self.model.set_node_common_properties(node_attrs) node._graph = self node.NODE_NAME = self.get_unique_name(node.NODE_NAME) node.model._graph_model = self.model node.model.name = node.NODE_NAME node.update() self._undo_stack.push(NodeAddedCmd(self, node, pos)) def delete_node(self, node): """ Remove the node from the node graph. Args: node (NodeGraphQt.BaseNode): node object. """ assert isinstance(node, NodeObject), \ 'node must be a instance of a NodeObject.' self.nodes_deleted.emit([node.id]) self._undo_stack.push(NodeRemovedCmd(self, node)) def delete_nodes(self, nodes): """ Remove a list of specified nodes from the node graph. Args: nodes (list[NodeGraphQt.BaseNode]): list of node instances. """ self.nodes_deleted.emit([n.id for n in nodes]) self._undo_stack.beginMacro('delete nodes') [self._undo_stack.push(NodeRemovedCmd(self, n)) for n in nodes] self._undo_stack.endMacro() def all_nodes(self): """ Return all nodes in the node graph. Returns: list[NodeGraphQt.BaseNode]: list of nodes. """ return list(self._model.nodes.values()) def selected_nodes(self): """ Return all selected nodes that are in the node graph. Returns: list[NodeGraphQt.BaseNode]: list of nodes. """ nodes = [] for item in self._viewer.selected_nodes(): node = self._model.nodes[item.id] nodes.append(node) return nodes def select_all(self): """ Select all nodes in the node graph. """ self._undo_stack.beginMacro('select all') for node in self.all_nodes(): node.set_selected(True) self._undo_stack.endMacro() def clear_selection(self): """ Clears the selection in the node graph. """ self._undo_stack.beginMacro('clear selection') for node in self.all_nodes(): node.set_selected(False) self._undo_stack.endMacro() def get_node_by_id(self, node_id=None): """ Returns the node from the node id string. Args: node_id (str): node id (:meth:`NodeObject.id`) Returns: NodeGraphQt.NodeObject: node object. """ return self._model.nodes.get(node_id) def get_node_by_name(self, name): """ Returns node that matches the name. Args: name (str): name of the node. Returns: NodeGraphQt.NodeObject: node object. """ for node_id, node in self._model.nodes.items(): if node.name() == name: return node def get_unique_name(self, name): """ Creates a unique node name to avoid having nodes with the same name. Args: name (str): node name. Returns: str: unique node name. """ name = ' '.join(name.split()) node_names = [n.name() for n in self.all_nodes()] if name not in node_names: return name regex = re.compile('[\w ]+(?: )*(\d+)') search = regex.search(name) if not search: for x in range(1, len(node_names) + 1): new_name = '{} {}'.format(name, x) if new_name not in node_names: return new_name version = search.group(1) name = name[:len(version) * -1].strip() for x in range(1, len(node_names) + 1): new_name = '{} {}'.format(name, x) if new_name not in node_names: return new_name def current_session(self): """ Returns the file path to the currently loaded session. Returns: str: path to the currently loaded session """ return self._model.session def clear_session(self): """ Clears the current node graph session. """ for n in self.all_nodes(): self.delete_node(n) self._undo_stack.clear() self._model.session = None def _serialize(self, nodes): """ serialize nodes to a dict. (used internally by the node graph) Args: nodes (list[NodeGraphQt.Nodes]): list of node instances. Returns: dict: serialized data. """ serial_data = {'nodes': {}, 'connections': []} nodes_data = {} for n in nodes: # update the node model. n.update_model() nodes_data.update(n.model.to_dict) for n_id, n_data in nodes_data.items(): serial_data['nodes'][n_id] = n_data inputs = n_data.pop('inputs') if n_data.get('inputs') else {} outputs = n_data.pop('outputs') if n_data.get('outputs') else {} for pname, conn_data in inputs.items(): for conn_id, prt_names in conn_data.items(): for conn_prt in prt_names: pipe = { 'in': [n_id, pname], 'out': [conn_id, conn_prt] } if pipe not in serial_data['connections']: serial_data['connections'].append(pipe) for pname, conn_data in outputs.items(): for conn_id, prt_names in conn_data.items(): for conn_prt in prt_names: pipe = { 'out': [n_id, pname], 'in': [conn_id, conn_prt] } if pipe not in serial_data['connections']: serial_data['connections'].append(pipe) if not serial_data['connections']: serial_data.pop('connections') return serial_data def _deserialize(self, data, relative_pos=False, pos=None): """ deserialize node data. (used internally by the node graph) Args: data (dict): node data. relative_pos (bool): position node relative to the cursor. Returns: list[NodeGraphQt.Nodes]: list of node instances. """ nodes = {} # build the nodes. for n_id, n_data in data.get('nodes', {}).items(): identifier = n_data['type_'] NodeCls = self._node_factory.create_node_instance(identifier) if NodeCls: node = NodeCls() node.NODE_NAME = n_data.get('name', node.NODE_NAME) # set properties. for prop in node.model.properties.keys(): if prop in n_data.keys(): node.model.set_property(prop, n_data[prop]) # set custom properties. for prop, val in n_data.get('custom', {}).items(): node.model.set_property(prop, val) nodes[n_id] = node self.add_node(node, n_data.get('pos')) # build the connections. for connection in data.get('connections', []): nid, pname = connection.get('in', ('', '')) in_node = nodes.get(nid) if not in_node: continue in_port = in_node.inputs().get(pname) if in_node else None nid, pname = connection.get('out', ('', '')) out_node = nodes.get(nid) if not out_node: continue out_port = out_node.outputs().get(pname) if out_node else None if in_port and out_port: self._undo_stack.push(PortConnectedCmd(in_port, out_port)) node_objs = list(nodes.values()) if relative_pos: self._viewer.move_nodes([n.view for n in node_objs]) [setattr(n.model, 'pos', n.view.xy_pos) for n in node_objs] elif pos: self._viewer.move_nodes([n.view for n in node_objs], pos=pos) [setattr(n.model, 'pos', n.view.xy_pos) for n in node_objs] return node_objs def serialize_session(self): """ Serializes the current node graph layout to a dictionary. Returns: dict: serialized session of the current node layout. """ return self._serialize(self.all_nodes()) def save_session(self, file_path): """ Saves the current node graph session layout to a `JSON` formatted file. Args: file_path (str): path to the saved node layout. """ serliazed_data = self._serialize(self.all_nodes()) file_path = file_path.strip() with open(file_path, 'w') as file_out: json.dump(serliazed_data, file_out, indent=2, separators=(',', ':')) def load_session(self, file_path): """ Load node graph session layout file. Args: file_path (str): path to the serialized layout file. """ file_path = file_path.strip() if not os.path.isfile(file_path): raise IOError('file does not exist.') self.clear_session() try: with open(file_path) as data_file: layout_data = json.load(data_file) except Exception as e: layout_data = None print('Cannot read data from file.\n{}'.format(e)) if not layout_data: return self._deserialize(layout_data) self._undo_stack.clear() self._model.session = file_path def copy_nodes(self, nodes=None): """ Copy nodes to the clipboard. Args: nodes (list[NodeGraphQt.BaseNode]): list of nodes (default: selected nodes). """ nodes = nodes or self.selected_nodes() if not nodes: return False clipboard = QtWidgets.QApplication.clipboard() serial_data = self._serialize(nodes) serial_str = json.dumps(serial_data) if serial_str: clipboard.setText(serial_str) return True return False def paste_nodes(self): """ Pastes nodes copied from the clipboard. """ clipboard = QtWidgets.QApplication.clipboard() cb_text = clipboard.text() if not cb_text: return self._undo_stack.beginMacro('pasted nodes') serial_data = json.loads(cb_text) self.clear_selection() nodes = self._deserialize(serial_data, relative_pos=True) [n.set_selected(True) for n in nodes] self._undo_stack.endMacro() def duplicate_nodes(self, nodes): """ Create duplicate copy from the list of nodes. Args: nodes (list[NodeGraphQt.BaseNode]): list of nodes. Returns: list[NodeGraphQt.BaseNode]: list of duplicated node instances. """ if not nodes: return self._undo_stack.beginMacro('duplicate nodes') self.clear_selection() serial = self._serialize(nodes) new_nodes = self._deserialize(serial) offset = 50 for n in new_nodes: x, y = n.pos() n.set_pos(x + offset, y + offset) n.set_property('selected', True) self._undo_stack.endMacro() return new_nodes def disable_nodes(self, nodes, mode=None): """ Set weather to Disable or Enable specified nodes. see: :meth:`NodeObject.set_disabled` Args: nodes (list[NodeGraphQt.BaseNode]): list of node instances. mode (bool): (optional) disable state of the nodes. """ if not nodes: return if mode is None: mode = not nodes[0].disabled() if len(nodes) > 1: text = {False: 'enable', True: 'disable'}[mode] text = '{} ({}) nodes'.format(text, len(nodes)) self._undo_stack.beginMacro(text) [n.set_disabled(mode) for n in nodes] self._undo_stack.endMacro() return nodes[0].set_disabled(mode) def question_dialog(self, text, title='Node Graph'): """ Prompts a question open dialog with "Yes" and "No" buttons in the node graph. (convenience function to :meth:`NodeGraph.viewer().question_dialog`) Args: text (str): question text. title (str): dialog window title. Returns: bool: true if user clicked yes. """ self._viewer.question_dialog(text, title) def message_dialog(self, text, title='Node Graph'): """ Prompts a file open dialog in the node graph. (convenience function to :meth:`NodeGraph.viewer().message_dialog`) Args: text (str): message text. title (str): dialog window title. """ self._viewer.message_dialog(text, title) def load_dialog(self, current_dir=None, ext=None): """ Prompts a file open dialog in the node graph. (convenience function to :meth:`NodeGraph.viewer().load_dialog`) Args: current_dir (str): path to a directory. ext (str): custom file type extension (default: json) Returns: str: selected file path. """ return self._viewer.load_dialog(current_dir, ext) def save_dialog(self, current_dir=None, ext=None): """ Prompts a file save dialog in the node graph. (convenience function to :meth:`NodeGraph.viewer().save_dialog`) Args: current_dir (str): path to a directory. ext (str): custom file type extension (default: json) Returns: str: selected file path. """ return self._viewer.save_dialog(current_dir, ext)
class PropertiesBinWidget(QtWidgets.QWidget): """ Node properties bin for displaying properties. Args: parent (QtWidgets.QWidget): parent of the new widget. node_graph (NodeGraphQt.NodeGraph): node graph. """ #: Signal emitted (node_id, prop_name, prop_value) property_changed = QtCore.Signal(str, str, object) def __init__(self, parent=None, node_graph=None): super(PropertiesBinWidget, self).__init__(parent) self.setWindowTitle('Properties Bin') self._prop_list = PropertiesList() self._limit = QtWidgets.QSpinBox() self._limit.setToolTip('Set display nodes limit.') self._limit.setMaximum(10) self._limit.setMinimum(0) self._limit.setValue(5) self._limit.valueChanged.connect(self.__on_limit_changed) self.resize(400, 400) self._block_signal = False btn_clr = QtWidgets.QPushButton('clear') btn_clr.setToolTip('Clear the properties bin.') btn_clr.clicked.connect(self.clear_bin) top_layout = QtWidgets.QHBoxLayout() top_layout.addWidget(self._limit) top_layout.addStretch(1) top_layout.addWidget(btn_clr) layout = QtWidgets.QVBoxLayout(self) layout.addLayout(top_layout) layout.addWidget(self._prop_list, 1) # wire up node graph. node_graph.add_properties_bin(self) node_graph.node_double_clicked.connect(self.add_node) node_graph.nodes_deleted.connect(self.__on_nodes_deleted) node_graph.property_changed.connect(self.__on_graph_property_changed) def __repr__(self): return '<{} object at {}>'.format(self.__class__.__name__, hex(id(self))) def __on_prop_close(self, node_id): items = self._prop_list.findItems(node_id, QtCore.Qt.MatchExactly) [self._prop_list.removeRow(i.row()) for i in items] def __on_limit_changed(self, value): rows = self._prop_list.rowCount() if rows > value: self._prop_list.removeRow(rows - 1) def __on_nodes_deleted(self, nodes): """ Slot function when a node has been deleted. Args: nodes (list[str]): list of node ids. """ [self.__on_prop_close(n) for n in nodes] def __on_graph_property_changed(self, node, prop_name, prop_value): """ Slot function that updates the property bin from the node graph signal. Args: node (NodeGraphQt.NodeObject): prop_name (str): prop_value (object): """ properties_widget = self.prop_widget(node) if not properties_widget: return property_window = properties_widget.get_widget(prop_name) if prop_value != property_window.get_value(): self._block_signal = True property_window.set_value(prop_value) self._block_signal = False def __on_property_widget_changed(self, node_id, prop_name, prop_value): """ Slot function triggered when a property widget value has changed. Args: node_id (str): prop_name (str): prop_value (object): """ if not self._block_signal: self.property_changed.emit(node_id, prop_name, prop_value) def limit(self): """ Returns the limit for how many nodes can be loaded into the bin. Returns: int: node limit. """ return int(self._limit.value()) def set_limit(self, limit): """ Set limit of nodes to display. Args: limit (int): node limit. """ self._limit.setValue(limit) def add_node(self, node): """ Add node to the properties bin. Args: node (NodeGraphQt.NodeObject): node object. """ if self.limit() == 0: return rows = self._prop_list.rowCount() if rows >= self.limit(): self._prop_list.removeRow(rows - 1) itm_find = self._prop_list.findItems(node.id, QtCore.Qt.MatchExactly) if itm_find: self._prop_list.removeRow(itm_find[0].row()) self._prop_list.insertRow(0) prop_widget = NodePropWidget(node=node) prop_widget.property_changed.connect(self.__on_property_widget_changed) prop_widget.property_closed.connect(self.__on_prop_close) self._prop_list.setCellWidget(0, 0, prop_widget) item = QtWidgets.QTableWidgetItem(node.id) self._prop_list.setItem(0, 0, item) self._prop_list.selectRow(0) def remove_node(self, node): """ Remove node from the properties bin. Args: node (str or NodeGraphQt.BaseNode): node id or node object. """ node_id = node if isinstance(node, str) else node.id self.__on_prop_close(node_id) def clear_bin(self): """ Clear the properties bin. """ self._prop_list.setRowCount(0) def prop_widget(self, node): """ Returns the node property widget. Args: node (str or NodeGraphQt.NodeObject): node id or node object. Returns: NodePropWidget: node property widget. """ node_id = node if isinstance(node, str) else node.id itm_find = self._prop_list.findItems(node_id, QtCore.Qt.MatchExactly) if itm_find: item = itm_find[0] return self._prop_list.cellWidget(item.row(), 0)