From 93593000e9ccda16fe47e5343be9909f21f24c17 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Tue, 6 Apr 2021 11:02:08 +1200 Subject: [PATCH 1/9] backdrop wrap undo block --- NodeGraphQt/base/node.py | 46 +++++++++++++++++++++++++++++++---- NodeGraphQt/widgets/viewer.py | 1 + 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/NodeGraphQt/base/node.py b/NodeGraphQt/base/node.py index ef95ecd5..e61765c3 100644 --- a/NodeGraphQt/base/node.py +++ b/NodeGraphQt/base/node.py @@ -1,4 +1,6 @@ #!/usr/bin/python +from collections import OrderedDict + from .commands import PropertyChangedCmd from .model import NodeModel from .port import Port @@ -1107,7 +1109,7 @@ def connected_input_nodes(self): Returns: dict: {: } """ - nodes = {} + nodes = OrderedDict() for p in self.input_ports(): nodes[p] = [cp.node() for cp in p.connected_ports()] return nodes @@ -1119,7 +1121,7 @@ def connected_output_nodes(self): Returns: dict: {: } """ - nodes = {} + nodes = OrderedDict() for p in self.output_ports(): nodes[p] = [cp.node() for cp in p.connected_ports()] return nodes @@ -1212,20 +1214,54 @@ def __init__(self): self.create_property('backdrop_text', '', widget_type=NODE_PROP_QTEXTEDIT, tab='Backdrop') + def on_backdrop_updated(self, update_prop, value=None): + """ + Slot triggered by the "on_backdrop_updated" signal from + the node graph. + + 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.view.auto_resize() + 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): """ - Wrap backdrop size to fit around specified nodes. + Set the backdrop size to fit around specified nodes. Args: nodes (list[NodeGraphQt.NodeObject]): list of nodes. """ - self.view.auto_resize([n.view for n in nodes]) + if not nodes: + return + self.graph.begin_undo('"{}" wrap nodes'.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 nodes(self): """ diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index 5f623ac9..c40312ba 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -38,6 +38,7 @@ class NodeViewer(QtWidgets.QGraphicsView): insert_node = QtCore.Signal(object, str, dict) need_show_tab_search = QtCore.Signal() node_name_changed = QtCore.Signal(str, str) + node_backdrop_updated = QtCore.Signal(str, str, object) # pass through signals that are translated into "NodeGraph()" signals. node_selected = QtCore.Signal(str) From c34cf06645c1419cf1f145f5490f0752051e1fbf Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Tue, 6 Apr 2021 11:03:23 +1200 Subject: [PATCH 2/9] question dialog bugfix --- NodeGraphQt/widgets/dialogs.py | 4 ++-- NodeGraphQt/widgets/stylesheet.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/NodeGraphQt/widgets/dialogs.py b/NodeGraphQt/widgets/dialogs.py index 14d0855a..7d1e36ce 100644 --- a/NodeGraphQt/widgets/dialogs.py +++ b/NodeGraphQt/widgets/dialogs.py @@ -64,5 +64,5 @@ def question_dialog(text, title): dlg.setStandardButtons( QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No ) - dlg.exec_() - return bool(dlg == QtWidgets.QMessageBox.Yes) + result = dlg.exec_() + return bool(result == QtWidgets.QMessageBox.Yes) diff --git a/NodeGraphQt/widgets/stylesheet.py b/NodeGraphQt/widgets/stylesheet.py index c0ee7407..57efc895 100644 --- a/NodeGraphQt/widgets/stylesheet.py +++ b/NodeGraphQt/widgets/stylesheet.py @@ -195,8 +195,7 @@ ''' STYLE_MESSAGEBOX = ''' -QLabel{ - min-width:500 px; +QLabel{ font-size: 18px; background-color: transparent; color: rgb(200 ,200, 200); From ded773cd36db73471201ebcc134633965fbedacf Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Tue, 6 Apr 2021 11:04:28 +1200 Subject: [PATCH 3/9] backdrop node wrap updates --- NodeGraphQt/qgraphics/node_backdrop.py | 67 +++++++++++++------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/NodeGraphQt/qgraphics/node_backdrop.py b/NodeGraphQt/qgraphics/node_backdrop.py index 8acbc968..a4353776 100644 --- a/NodeGraphQt/qgraphics/node_backdrop.py +++ b/NodeGraphQt/qgraphics/node_backdrop.py @@ -53,6 +53,19 @@ def itemChange(self, change, value): def mouseDoubleClickEvent(self, event): item = self.parentItem() item.on_sizer_double_clicked() + super(BackdropSizer, self).mouseDoubleClickEvent(event) + + def mousePressEvent(self, event): + self.__prev_xy = (self.pos().x(), self.pos().y()) + super(BackdropSizer, self).mousePressEvent(event) + + 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 paint(self, painter, option, widget): """ @@ -143,8 +156,18 @@ def on_sizer_pos_changed(self, pos): self._width = pos.x() + self._sizer.size self._height = pos.y() + self._sizer.size + 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) + def on_sizer_double_clicked(self): - self.auto_resize() + size = self.calc_backdrop_size() + self.viewer().node_backdrop_updated.emit( + self.id, 'sizer_double_clicked', size) def paint(self, painter, option, widget): """ @@ -217,39 +240,17 @@ def get_nodes(self, inc_intersects=False): nodes.append(item) return nodes - def auto_resize(self, nodes=None): + def calc_backdrop_size(self, nodes=None): nodes = nodes or self.get_nodes(True) - if nodes: - padding = 40 - nodes_rect = self._combined_rect(nodes) - self.xy_pos = [nodes_rect.x() - padding, nodes_rect.y() - padding] - self._sizer.set_pos(nodes_rect.width() + (padding * 2), - nodes_rect.height() + (padding * 2)) - return - - width, height = self._min_size - self._sizer.set_pos(width, height) - - def pre_init(self, viewer, pos=None): - """ - Called before node has been added into the scene. - - Args: - viewer (NodeGraphQt.widgets.viewer.NodeViewer): main viewer. - pos (tuple): cursor pos. - """ - nodes = viewer.selected_nodes() - if nodes: - padding = 40 - scene = viewer.scene() - group = scene.createItemGroup(nodes) - rect = group.boundingRect() - scene.destroyItemGroup(group) - self.xy_pos = [rect.x() - padding, rect.y() - padding] - self._sizer.set_pos(rect.width() + (padding * 2), - rect.height() + (padding * 2)) - else: - self.xy_pos = pos + padding = 40 + nodes_rect = self._combined_rect(nodes) + 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): From 8c9fba66f7d057d6783f5ecd6bd9a3a5c2511c3c Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Tue, 6 Apr 2021 23:33:29 +1200 Subject: [PATCH 4/9] refactored auto node layout logic into graph. --- NodeGraphQt/base/graph.py | 175 ++++++++++++++++++++++++++++-- NodeGraphQt/base/model.py | 3 +- NodeGraphQt/base/node.py | 2 +- NodeGraphQt/base/utils.py | 223 ++------------------------------------ example.py | 43 ++++---- 5 files changed, 199 insertions(+), 247 deletions(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index f0ab1d5d..d5ff84af 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -15,14 +15,15 @@ from .factory import NodeFactory from .menu import NodeGraphMenu, NodesMenu from .model import NodeGraphModel -from .node import NodeObject, BaseNode +from .node import NodeObject, BaseNode, BackdropNode from .port import Port -from ..constants import (DRAG_DROP_ID, - PIPE_LAYOUT_CURVED, - PIPE_LAYOUT_STRAIGHT, - PIPE_LAYOUT_ANGLE, - IN_PORT, OUT_PORT, - VIEWER_GRID_LINES) +from ..constants import ( + DRAG_DROP_ID, + NODE_LAYOUT_DIRECTION, NODE_LAYOUT_VERTICAL, NODE_LAYOUT_HORIZONTAL, + PIPE_LAYOUT_CURVED, PIPE_LAYOUT_STRAIGHT, PIPE_LAYOUT_ANGLE, + IN_PORT, OUT_PORT, + VIEWER_GRID_LINES +) from ..widgets.node_space_bar import node_space_bar from ..widgets.viewer import NodeViewer @@ -182,7 +183,9 @@ def _wire_signals(self): self._viewer.moved_nodes.connect(self._on_nodes_moved) self._viewer.node_double_clicked.connect(self._on_node_double_clicked) self._viewer.node_name_changed.connect(self._on_node_name_changed) - self._viewer.insert_node.connect(self._insert_node) + self._viewer.node_backdrop_updated.connect( + self._on_node_backdrop_updated) + self._viewer.insert_node.connect(self._on_insert_node) # pass through translated signals. self._viewer.node_selected.connect(self._on_node_selected) @@ -190,7 +193,7 @@ def _wire_signals(self): self._on_node_selection_changed) self._viewer.data_dropped.connect(self._on_node_data_dropped) - def _insert_node(self, pipe, node_id, prev_node_pos): + def _on_insert_node(self, pipe, node_id, prev_node_pos): """ Slot function triggered when a selected node has collided with a pipe. @@ -344,6 +347,18 @@ def _on_nodes_moved(self, node_data): self._undo_stack.push(NodeMovedCmd(node, node.pos(), prev_pos)) self._undo_stack.endMacro() + def _on_node_backdrop_updated(self, node_id, update_property, value): + """ + called when a BackdropNode is updated. + + Args: + node_id (str): backdrop node id. + value (str): update type. + """ + backdrop = self.get_node_by_id(node_id) + if backdrop and isinstance(backdrop, BackdropNode): + backdrop.on_backdrop_updated(update_property, value) + def _on_search_triggered(self, node_type, pos): """ called when the tab search widget is triggered in the viewer. @@ -859,7 +874,7 @@ def create_node(self, node_type, name=None, selected=True, color=None, pos (list[int, int]): initial x, y position for the node (default: ``(0, 0)``). Returns: - NodeGraphQt.BaseNode: the created instance of the node. + NodeGraphQt.NodeObject: the created instance of the node. """ if not self._editable: return @@ -903,6 +918,7 @@ def format_color(clr): else: node.set_parent(None) + # update the node view from model. node.update() undo_cmd = NodeAddedCmd(self, node, node.model.pos) @@ -1552,6 +1568,143 @@ def disable_nodes(self, nodes, mode=None): return nodes[0].set_disabled(mode) + # auto layout node functions. + + @staticmethod + def _update_node_rank(node, nodes_rank, down_stream=True): + """ + Recursive function for updating the node ranking. + + Args: + node (NodeGraphQt.BaseNode): node to start from. + nodes_rank (dict): node ranking object to be updated. + down_stream (bool): true to rank down stram. + """ + if down_stream: + node_values = node.connected_output_nodes().values() + else: + node_values = node.connected_input_nodes().values() + + connected_nodes = set() + for nodes in node_values: + connected_nodes.update(nodes) + + rank = nodes_rank[node] + 1 + for n in connected_nodes: + if n in nodes_rank: + nodes_rank[n] = max(nodes_rank[n], rank) + else: + nodes_rank[n] = rank + NodeGraph._update_node_rank(n, nodes_rank, down_stream) + + @staticmethod + def _compute_node_rank(nodes, down_stream=True): + """ + Compute the ranking of nodes. + + Args: + nodes (list[NodeGraphQt.BaseNode]): nodes to start ranking from. + down_stream (bool): true to compute down stream. + + Returns: + dict: {NodeGraphQt.BaseNode: node_rank, ...} + """ + nodes_rank = {} + for node in nodes: + nodes_rank[node] = 0 + NodeGraph._update_node_rank(node, nodes_rank, down_stream) + return nodes_rank + + def auto_layout_nodes(self, nodes=None, down_stream=True): + """ + Auto layout the nodes in the node graph. + + Args: + nodes (list[NodeGraphQt.BaseNode]): list of nodes to auto layout + if nodes is None then all nodes is layed out. + down_stream (bool): false to layout up stream. + """ + self.begin_undo('Auto Layout Nodes') + + nodes = nodes or self.all_nodes() + + # filter out the backdrops. + backdrops = { + n: n.nodes() for n in nodes if isinstance(n, BackdropNode) + } + filtered_nodes = [n for n in nodes if not isinstance(n, BackdropNode)] + + start_nodes = [] + if down_stream: + start_nodes += [ + n for n in filtered_nodes + if not any(n.connected_input_nodes().values()) + ] + else: + start_nodes += [ + n for n in filtered_nodes + if not any(n.connected_output_nodes().values()) + ] + + if not start_nodes: + return + + node_views = [n.view for n in nodes] + nodes_center_0 = self.viewer().nodes_rect_center(node_views) + + nodes_rank = NodeGraph._compute_node_rank(start_nodes, down_stream) + + rank_map = {} + for node, rank in nodes_rank.items(): + if rank in rank_map: + rank_map[rank].append(node) + else: + rank_map[rank] = [node] + + if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: + current_x = 0 + node_height = 120 + for rank in sorted(range(len(rank_map)), reverse=not down_stream): + ranked_nodes = rank_map[rank] + max_width = max([node.view.width for node in ranked_nodes]) + current_x += max_width + current_y = 0 + for idx, node in enumerate(ranked_nodes): + dy = max(node_height, node.view.height) + current_y += 0 if idx == 0 else dy + node.set_pos(current_x, current_y) + current_y += dy * 0.5 + 10 + + current_x += max_width * 0.5 + 100 + elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: + current_y = 0 + node_width = 250 + for rank in sorted(range(len(rank_map)), reverse=not down_stream): + ranked_nodes = rank_map[rank] + max_height = max([node.view.height for node in ranked_nodes]) + current_y += max_height + current_x = 0 + for idx, node in enumerate(ranked_nodes): + dx = max(node_width, node.view.width) + current_x += 0 if idx == 0 else dx + node.set_pos(current_x, current_y) + current_x += dx * 0.5 + 10 + + current_y += max_height * 0.5 + 100 + + nodes_center_1 = self.viewer().nodes_rect_center(node_views) + dx = nodes_center_0[0] - nodes_center_1[0] + dy = nodes_center_0[1] - nodes_center_1[1] + [n.set_pos(n.x_pos() + dx, n.y_pos() + dy) for n in nodes] + + # wrap the backdrop nodes. + for backdrop, contained_nodes in backdrops.items(): + backdrop.wrap_nodes(contained_nodes) + + self.end_undo() + + # prompt dialog functions. + def question_dialog(self, text, title='Node Graph'): """ Prompts a question open dialog with ``"Yes"`` and ``"No"`` buttons in @@ -1618,6 +1771,8 @@ def save_dialog(self, current_dir=None, ext=None): """ return self._viewer.save_dialog(current_dir, ext) + # hmmm... refactor functions below for "GroupNode" not "SubGraph". + def use_OpenGL(self): """ Use OpenGL to draw the graph. diff --git a/NodeGraphQt/base/model.py b/NodeGraphQt/base/model.py index 87f45ee5..4af3074c 100644 --- a/NodeGraphQt/base/model.py +++ b/NodeGraphQt/base/model.py @@ -160,8 +160,7 @@ def set_property(self, name, value): elif name in self._custom_prop.keys(): self._custom_prop[name] = value else: - self.add_property(name, value) - # raise NodePropertyError('No property "{}"'.format(name)) + raise NodePropertyError('No property "{}"'.format(name)) def get_property(self, name): if name in self.properties.keys(): diff --git a/NodeGraphQt/base/node.py b/NodeGraphQt/base/node.py index e61765c3..31ab5836 100644 --- a/NodeGraphQt/base/node.py +++ b/NodeGraphQt/base/node.py @@ -1257,7 +1257,7 @@ def wrap_nodes(self, nodes): if not nodes: return self.graph.begin_undo('"{}" wrap nodes'.format(self.name())) - size = self.view.calc_backdrop_size() + 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']) diff --git a/NodeGraphQt/base/utils.py b/NodeGraphQt/base/utils.py index 5ccdc71c..045ddb7b 100644 --- a/NodeGraphQt/base/utils.py +++ b/NodeGraphQt/base/utils.py @@ -86,8 +86,10 @@ def setup_context_menu(graph): edit_menu.add_separator() - edit_menu.add_command('Layout Graph Up Stream', _layout_graph_up, 'L') - edit_menu.add_command('Layout Graph Down Stream', _layout_graph_down, 'Ctrl+L') + edit_menu.add_command( + 'Layout Nodes Up Stream', _layout_graph_up, 'L') + edit_menu.add_command( + 'Layout Nodes Down Stream', _layout_graph_down, 'Ctrl+L') edit_menu.add_separator() @@ -300,34 +302,15 @@ def _bg_grid_lines(graph): graph.set_grid_mode(2) -def __layout_graph(graph, down_stream=True): - graph.begin_undo('Auto Layout') - node_space = graph.get_node_space() - if node_space is not None: - nodes = node_space.children() - else: - nodes = graph.all_nodes() - if not nodes: - return - node_views = [n.view for n in nodes] - nodes_center0 = graph.viewer().nodes_rect_center(node_views) - if down_stream: - auto_layout_down(all_nodes=nodes) - else: - auto_layout_up(all_nodes=nodes) - nodes_center1 = graph.viewer().nodes_rect_center(node_views) - dx = nodes_center0[0] - nodes_center1[0] - dy = nodes_center0[1] - nodes_center1[1] - [n.set_pos(n.x_pos() + dx, n.y_pos()+dy) for n in nodes] - graph.end_undo() - - def _layout_graph_down(graph): - __layout_graph(graph, True) + nodes = graph.selected_nodes() or graph.all_nodes() + graph.auto_layout_nodes(nodes=nodes, down_stream=True) def _layout_graph_up(graph): - __layout_graph(graph, False) + nodes = graph.selected_nodes() or graph.all_nodes() + graph.auto_layout_nodes(nodes-nodes, down_stream=False) + # topological_sort @@ -626,194 +609,6 @@ def update_nodes_by_up(nodes): _update_nodes(topological_sort_by_up(all_nodes=nodes)) -# auto layout - - -def _update_node_rank_down(node, nodes_rank): - rank = nodes_rank[node] + 1 - for n in get_output_nodes(node, False): - if n in nodes_rank: - nodes_rank[n] = max(nodes_rank[n], rank) - else: - nodes_rank[n] = rank - _update_node_rank_down(n, nodes_rank) - - -def _compute_rank_down(start_nodes): - """ - Compute the rank of the down stream nodes. - - Args: - start_nodes (list[NodeGraphQt.BaseNode]): - (Optional) the start nodes of the graph. - - Returns: - dict{NodeGraphQt.BaseNode: node_rank, ...} - """ - nodes_rank = {} - for node in start_nodes: - nodes_rank[node] = 0 - _update_node_rank_down(node, nodes_rank) - return nodes_rank - - -def _update_node_rank_up(node, nodes_rank): - rank = nodes_rank[node] + 1 - for n in get_input_nodes(node): - if n in nodes_rank: - nodes_rank[n] = max(nodes_rank[n], rank) - else: - nodes_rank[n] = rank - _update_node_rank_up(n, nodes_rank) - - -def _compute_rank_up(start_nodes): - """ - Compute the rank of the up stream nodes. - - Args: - start_nodes (list[NodeGraphQt.BaseNode]): - (Optional) the end nodes of the graph. - - Returns: - dict{NodeGraphQt.BaseNode: node_rank, ...} - """ - - nodes_rank = {} - for node in start_nodes: - nodes_rank[node] = 0 - _update_node_rank_up(node, nodes_rank) - return nodes_rank - - -def auto_layout_up(start_nodes=None, all_nodes=None): - """ - Auto layout the nodes by up stream direction. - - Args: - start_nodes (list[NodeGraphQt.BaseNode]): - (Optional) the end nodes of the graph. - all_nodes (list[NodeGraphQt.BaseNode]): - (Optional) if 'start_nodes' is None the function can calculate - start nodes from 'all_nodes'. - """ - if not start_nodes and not all_nodes: - return - if start_nodes: - start_nodes = __remove_BackdropNode(start_nodes) - if all_nodes: - all_nodes = __remove_BackdropNode(all_nodes) - - if not start_nodes: - start_nodes = [n for n in all_nodes if not _has_output_node(n)] - if not start_nodes: - return - - nodes_rank = _compute_rank_up(start_nodes) - - rank_map = {} - for node, rank in nodes_rank.items(): - if rank in rank_map: - rank_map[rank].append(node) - else: - rank_map[rank] = [node] - - if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: - current_x = 0 - node_height = 80 - for rank in reversed(range(len(rank_map))): - nodes = rank_map[rank] - max_width = max([node.view.width for node in nodes]) - current_x += max_width - current_y = 0 - for idx, node in enumerate(nodes): - dy = max(node_height, node.view.height) - current_y += 0 if idx == 0 else dy - node.set_pos(current_x, current_y) - current_y += dy * 0.5 + 10 - - current_x += max_width * 0.5 + 100 - elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: - current_y = 0 - node_width = 250 - for rank in reversed(range(len(rank_map))): - nodes = rank_map[rank] - max_height = max([node.view.height for node in nodes]) - current_y += max_height - current_x = 0 - for idx, node in enumerate(nodes): - dx = max(node_width, node.view.width) - current_x += 0 if idx == 0 else dx - node.set_pos(current_x, current_y) - current_x += dx * 0.5 + 10 - - current_y += max_height * 0.5 + 100 - - -def auto_layout_down(start_nodes=None, all_nodes=None): - """ - Auto layout the nodes by down stream direction. - - Args: - start_nodes (list[NodeGraphQt.BaseNode]): - (Optional) the start update nodes of the graph. - all_nodes (list[NodeGraphQt.BaseNode]): - (Optional) if 'start_nodes' is None the function can calculate - start nodes from 'all_nodes'. - """ - if not start_nodes and not all_nodes: - return - if start_nodes: - start_nodes = __remove_BackdropNode(start_nodes) - if all_nodes: - all_nodes = __remove_BackdropNode(all_nodes) - - if not start_nodes: - start_nodes = [n for n in all_nodes if not _has_input_node(n)] - if not start_nodes: - return - - nodes_rank = _compute_rank_down(start_nodes) - - rank_map = {} - for node, rank in nodes_rank.items(): - if rank in rank_map: - rank_map[rank].append(node) - else: - rank_map[rank] = [node] - - if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: - current_x = 0 - node_height = 120 - for rank in range(len(rank_map)): - nodes = rank_map[rank] - max_width = max([node.view.width for node in nodes]) - current_x += max_width - current_y = 0 - for idx, node in enumerate(nodes): - dy = max(node_height, node.view.height) - current_y += 0 if idx == 0 else dy - node.set_pos(current_x, current_y) - current_y += dy * 0.5 + 10 - - current_x += max_width * 0.5 + 100 - elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: - current_y = 0 - node_width = 250 - for rank in range(len(rank_map)): - nodes = rank_map[rank] - max_height = max([node.view.height for node in nodes]) - current_y += max_height - current_x = 0 - for idx, node in enumerate(nodes): - dx = max(node_width, node.view.width) - current_x += 0 if idx == 0 else dx - node.set_pos(current_x, current_y) - current_x += dx * 0.5 + 10 - - current_y += max_height * 0.5 + 100 - - # garbage collect def minimize_node_ref_count(node): diff --git a/example.py b/example.py index a9d8fce8..8a7b2dac 100644 --- a/example.py +++ b/example.py @@ -179,31 +179,32 @@ def show_nodes_list(node): for n in reg_nodes: graph.register_node(n) - my_node = graph.create_node('com.chantasticvfx.MyNode', - name='chantastic!', - color='#0a1e20', - text_color='#feab20', - pos=[310, 10]) - - foo_node = graph.create_node('com.chantasticvfx.FooNode', - name='node', - pos=[-480, 140]) + my_node = graph.create_node( + 'com.chantasticvfx.MyNode', + name='chantastic!', + color='#0a1e20', + text_color='#feab20' + ) + + foo_node = graph.create_node( + 'com.chantasticvfx.FooNode', + name='node') foo_node.set_disabled(True) # create example "TextInputNode". - text_node = graph.create_node('com.chantasticvfx.TextInputNode', - name='text node', - pos=[-480, -160]) + text_node = graph.create_node( + 'com.chantasticvfx.TextInputNode', + name='text node') # create example "TextInputNode". - checkbox_node = graph.create_node('com.chantasticvfx.CheckboxNode', - name='checkbox node', - pos=[-480, -20]) + checkbox_node = graph.create_node( + 'com.chantasticvfx.CheckboxNode', + name='checkbox node') # create node with a combo box menu. - menu_node = graph.create_node('com.chantasticvfx.DropdownMenuNode', - name='menu node', - pos=[280, -200]) + menu_node = graph.create_node( + 'com.chantasticvfx.DropdownMenuNode', + name='menu node') # change node icon. this_path = os.path.dirname(os.path.abspath(__file__)) @@ -211,16 +212,18 @@ def show_nodes_list(node): bar_node = graph.create_node('com.chantasticvfx.BarNode') bar_node.set_icon(icon) bar_node.set_name('icon node') - bar_node.set_pos(-70, 10) # connect the nodes. foo_node.set_output(0, bar_node.input(2)) menu_node.set_input(0, bar_node.output(1)) bar_node.set_input(0, text_node.output(0)) + # auto layout nodes. + graph.auto_layout_nodes() + # wrap a backdrop node. backdrop_node = graph.create_node('nodeGraphQt.nodes.BackdropNode') - backdrop_node.wrap_nodes([my_node, menu_node]) + backdrop_node.wrap_nodes([text_node, checkbox_node]) graph.fit_to_selection() From 733c08f431a864120ac85c60dfcd0c1dc7c84b78 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Tue, 6 Apr 2021 23:48:27 +1200 Subject: [PATCH 5/9] backdrop bug fix --- NodeGraphQt/qgraphics/node_backdrop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NodeGraphQt/qgraphics/node_backdrop.py b/NodeGraphQt/qgraphics/node_backdrop.py index a4353776..b0f47a2d 100644 --- a/NodeGraphQt/qgraphics/node_backdrop.py +++ b/NodeGraphQt/qgraphics/node_backdrop.py @@ -61,7 +61,7 @@ def mousePressEvent(self, event): def mouseReleaseEvent(self, event): current_xy = (self.pos().x(), self.pos().y()) - if current_xy != self._prev_xy: + if current_xy != self.__prev_xy: item = self.parentItem() item.on_sizer_pos_mouse_release() del self.__prev_xy From 168a2953030cce0914b16b6f4a9f1af141ea14a1 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Sat, 10 Apr 2021 20:52:23 +1200 Subject: [PATCH 6/9] tweaks --- NodeGraphQt/base/graph.py | 13 +++++++------ NodeGraphQt/base/utils.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index 3f134096..cd406a0f 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -17,12 +17,13 @@ from .model import NodeGraphModel from .node import NodeObject, BaseNode, BackdropNode from .port import Port -from ..constants import (URI_SCHEME, URN_SCHEME, - PIPE_LAYOUT_CURVED, - PIPE_LAYOUT_STRAIGHT, - PIPE_LAYOUT_ANGLE, - IN_PORT, OUT_PORT, - VIEWER_GRID_LINES) +from ..constants import ( + URI_SCHEME, URN_SCHEME, + NODE_LAYOUT_DIRECTION, NODE_LAYOUT_HORIZONTAL, NODE_LAYOUT_VERTICAL, + PIPE_LAYOUT_CURVED, PIPE_LAYOUT_STRAIGHT, PIPE_LAYOUT_ANGLE, + IN_PORT, OUT_PORT, + VIEWER_GRID_LINES +) from ..widgets.node_space_bar import node_space_bar from ..widgets.viewer import NodeViewer diff --git a/NodeGraphQt/base/utils.py b/NodeGraphQt/base/utils.py index 045ddb7b..78187336 100644 --- a/NodeGraphQt/base/utils.py +++ b/NodeGraphQt/base/utils.py @@ -87,9 +87,9 @@ def setup_context_menu(graph): edit_menu.add_separator() edit_menu.add_command( - 'Layout Nodes Up Stream', _layout_graph_up, 'L') + 'Auto Layout Up Stream', _layout_graph_up, 'L') edit_menu.add_command( - 'Layout Nodes Down Stream', _layout_graph_down, 'Ctrl+L') + 'Auto Layout Down Stream', _layout_graph_down, 'Ctrl+L') edit_menu.add_separator() From b0b531816f42c7b616a3aab672c98aa68ff28892 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Sat, 10 Apr 2021 20:53:58 +1200 Subject: [PATCH 7/9] readme example update. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 624d515e..9a6e651a 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,10 @@ if __name__ == '__main__': node_b = graph.create_node('com.chantasticvfx.MyNode', name='Node B', color='#5b162f') # connect node a input to node b output. - node_a.set_input(0, node_b.output(0)) + node_a.set_input(0, node_b.output(0)) + + # auto layout nodes. + graph.auto_layout_nodes() # get the widget and show. graph_widget = graph.widget From 457b735009a756b1e5c18f36d888d96429b18204 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Sun, 11 Apr 2021 16:08:54 +1200 Subject: [PATCH 8/9] comment --- NodeGraphQt/base/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index cd406a0f..e78fa717 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -1762,7 +1762,7 @@ def save_dialog(self, current_dir=None, ext=None): """ return self._viewer.save_dialog(current_dir, ext) - # hmmm... refactor functions below for "GroupNode" not "SubGraph". + ### --- def use_OpenGL(self): """ From 11e8bfd2ab4bf8ba50499c9a04f87087aac7f613 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Sun, 11 Apr 2021 16:18:13 +1200 Subject: [PATCH 9/9] updated doc strings --- NodeGraphQt/base/graph.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index e78fa717..f7e67c22 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -1606,14 +1606,20 @@ def _compute_node_rank(nodes, down_stream=True): NodeGraph._update_node_rank(node, nodes_rank, down_stream) return nodes_rank - def auto_layout_nodes(self, nodes=None, down_stream=True): + def auto_layout_nodes(self, nodes=None, down_stream=True, start_nodes=None): """ Auto layout the nodes in the node graph. + Note: + If the node graph is acyclic then the ``start_nodes`` will need + to be specified. + Args: nodes (list[NodeGraphQt.BaseNode]): list of nodes to auto layout if nodes is None then all nodes is layed out. down_stream (bool): false to layout up stream. + start_nodes (list[NodeGraphQt.BaseNode]): + list of nodes to start the auto layout from (Optional). """ self.begin_undo('Auto Layout Nodes') @@ -1625,7 +1631,7 @@ def auto_layout_nodes(self, nodes=None, down_stream=True): } filtered_nodes = [n for n in nodes if not isinstance(n, BackdropNode)] - start_nodes = [] + start_nodes = start_nodes or [] if down_stream: start_nodes += [ n for n in filtered_nodes