diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index 27467b83..31b81ca4 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -137,6 +137,7 @@ def __init__(self, parent=None): super(NodeGraph, self).__init__(parent) self.setObjectName('NodeGraphQt') self._widget = None + self._undo_view = None self._model = NodeGraphModel() self._viewer = NodeViewer() self._node_factory = NodeFactory() @@ -387,6 +388,19 @@ def widget(self): layout.addWidget(self._viewer) return self._widget + @property + def undo_view(self): + """ + Returns node graph undo view. + + Returns: + PySide2.QtWidgets.QUndoView: node graph undo view. + """ + if self._undo_view is None: + self._undo_view = QtWidgets.QUndoView(self._undo_stack) + self._undo_view.setWindowTitle("Undo View") + return self._undo_view + @property def auto_update(self): """ @@ -1358,9 +1372,11 @@ def cut_nodes(self, nodes=None): Args: nodes (list[NodeGraphQt.BaseNode]): list of nodes (default: selected nodes). """ + self._undo_stack.beginMacro('cut nodes') nodes = nodes or self.selected_nodes() self.copy_nodes(nodes) self.delete_nodes(nodes) + self._undo_stack.endMacro() def paste_nodes(self): """ diff --git a/NodeGraphQt/base/node.py b/NodeGraphQt/base/node.py index cc8dc368..6c2079f3 100644 --- a/NodeGraphQt/base/node.py +++ b/NodeGraphQt/base/node.py @@ -1088,12 +1088,6 @@ def run(self): """ return - def when_disabled(self): - """ - Node evaluation logic when node has been disabled. - """ - return - def set_editable(self, state): """ Returns whether the node view widgets is editable. @@ -1104,6 +1098,16 @@ def set_editable(self, state): [wid.setEnabled(state) for wid in self.view._widgets.values()] self.view.text_item.setEnabled(state) + def set_dynamic_port(self, state): + """ + Set whether the node will delete/add port after node has been created. + + Args: + state(bool): If True, all port data will be serialized with the node, + when the node is been deserialized, all ports will restore. + """ + self.model.dynamic_port = state + class BackdropNode(NodeObject): """ diff --git a/NodeGraphQt/base/port.py b/NodeGraphQt/base/port.py index af010439..eb995916 100644 --- a/NodeGraphQt/base/port.py +++ b/NodeGraphQt/base/port.py @@ -221,7 +221,6 @@ def data_type(self): def data_type(self, data_type): self.__model.data_type = data_type - @property def border_color(self): return self.__view.border_color diff --git a/NodeGraphQt/base/utils.py b/NodeGraphQt/base/utils.py index 3703ed97..dc4c5a2c 100644 --- a/NodeGraphQt/base/utils.py +++ b/NodeGraphQt/base/utils.py @@ -66,6 +66,7 @@ def setup_context_menu(graph): edit_menu.add_separator() edit_menu.add_command('Clear Undo History', _clear_undo) + edit_menu.add_command('Show Undo View', _show_undo_view) edit_menu.add_separator() edit_menu.add_command('Copy', _copy_nodes, QtGui.QKeySequence.Copy) @@ -270,6 +271,10 @@ def _jump_out(graph): graph.set_node_space(node.parent()) +def _show_undo_view(graph): + graph.undo_view.show() + + def _curved_pipe(graph): graph.set_pipe_style(PIPE_LAYOUT_CURVED) @@ -560,11 +565,10 @@ def _update_nodes(nodes): nodes (list[NodeGraphQt.BaseNode]): nodes to be run. """ for node in nodes: + if node.disabled(): + continue try: - if node.disabled(): - node.when_disabled() - else: - node.run() + node.run() except Exception as error: print("Error Update Node : {}\n{}" .format(node, str(error))) break diff --git a/example_auto_nodes.py b/example_auto_nodes.py index fb3bbed9..0c96f03b 100644 --- a/example_auto_nodes.py +++ b/example_auto_nodes.py @@ -1,15 +1,12 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import example_auto_nodes -from NodeGraphQt import NodeGraph, setup_context_menu -from NodeGraphQt import QtWidgets, QtCore, PropertiesBinWidget, \ - NodeTreeWidget, BackdropNode, NodePublishWidget -import os -import sys -import inspect +from NodeGraphQt import NodeGraph, setup_context_menu, \ + QtWidgets, QtCore, PropertiesBinWidget, BackdropNode +from example_auto_nodes import Publish, RootNode, update_nodes, setup_node_menu import importlib -from example_auto_nodes import AutoNode, ModuleNode, \ - SubGraphNode, Publish, RootNode, update_nodes +import inspect +import sys +import os def get_nodes_from_folder(folder_path): @@ -49,51 +46,8 @@ def get_published_nodes_from_folder(folder_path): return nodes -def cook_node(graph, node): - node.update_stream(forceCook=True) - - -def print_functions(graph, node): - for func in node.module_functions: - print(func) - - -def toggle_auto_cook(graph, node): - node.auto_cook = not node.auto_cook - - -def enter_node(graph, node): - graph.set_node_space(node) - - -def allow_edit(graph, node): - node.set_property('published', False) - - -def print_path(graph, node): - print(node.path()) - - -def find_node_by_path(graph, node): - print(graph.get_node_by_path(node.path())) - - -def print_children(graph, node): - children = node.children() - print(len(children), children) - - -def publish_node(graph, node): - wid = NodePublishWidget(node=node) - wid.show() - - -def cook_nodes(nodes): - update_nodes(nodes) - - if __name__ == '__main__': - app = QtWidgets.QApplication([]) + app = QtWidgets.QApplication() # create node graph. graph = NodeGraph() @@ -101,6 +55,7 @@ def cook_nodes(nodes): # set up default menu and commands. setup_context_menu(graph) + setup_node_menu(graph, Publish) # show the properties bin when a node is "double clicked" in the graph. properties_bin = PropertiesBinWidget(node_graph=graph) @@ -111,15 +66,6 @@ def show_prop_bin(node): properties_bin.show() graph.node_double_clicked.connect(show_prop_bin) - # show the nodes list when a node is "double clicked" in the graph. - node_tree = NodeTreeWidget(node_graph=graph) - - def show_nodes_list(node): - if not node_tree.isVisible(): - node_tree.update() - node_tree.show() - graph.node_double_clicked.connect(show_nodes_list) - # register nodes reg_nodes = get_nodes_from_folder(os.getcwd() + "/example_auto_nodes") BackdropNode.__identifier__ = 'Utility::Backdrop' @@ -127,24 +73,13 @@ def show_nodes_list(node): reg_nodes.extend(get_published_nodes_from_folder(os.getcwd() + "/example_auto_nodes/published_nodes")) [graph.register_node(n) for n in reg_nodes] - # setup node menu - node_menu = graph.context_nodes_menu() - node_menu.add_command('Allow Edit', allow_edit, node_class=Publish) - node_menu.add_command('Enter Node', enter_node, node_class=SubGraphNode) - node_menu.add_command('Publish Node', publish_node, node_class=SubGraphNode) - node_menu.add_command('Print Children', print_children, node_class=SubGraphNode) - node_menu.add_command('Print Functions', print_functions, node_class=ModuleNode) - node_menu.add_command('Cook Node', cook_node, node_class=AutoNode) - node_menu.add_command('Toggle Auto Cook', toggle_auto_cook, node_class=AutoNode) - node_menu.add_command('Print Path', print_path, node_class=AutoNode) - node_menu.add_command('Find Node By Path', find_node_by_path, node_class=AutoNode) - # create root node + # if we want to use sub graph system, root node is must. graph.add_node(RootNode()) # create test nodes graph.load_session(r'example_auto_nodes/networks/example_SubGraph.json') - cook_nodes(graph.root_node().children()) + update_nodes(graph.root_node().children()) # widget used for the node graph. graph_widget = graph.widget diff --git a/example_auto_nodes/__init__.py b/example_auto_nodes/__init__.py index a55742b9..ef4f5cbc 100644 --- a/example_auto_nodes/__init__.py +++ b/example_auto_nodes/__init__.py @@ -1,5 +1,3 @@ -from .node_base.auto_node import AutoNode -from .node_base.module_node import ModuleNode -from .node_base.subgraph_node import SubGraphNode, RootNode +from .node_base import AutoNode, ModuleNode, SubGraphNode, RootNode from .subgraph_nodes import Publish -from .node_base.utils import update_node_down_stream, update_nodes +from .node_base.utils import update_node_down_stream, update_nodes, setup_node_menu diff --git a/example_auto_nodes/basic_nodes.py b/example_auto_nodes/basic_nodes.py index b731020e..2e5e1432 100644 --- a/example_auto_nodes/basic_nodes.py +++ b/example_auto_nodes/basic_nodes.py @@ -1,4 +1,4 @@ -from .node_base.auto_node import AutoNode +from .node_base import AutoNode class FooNode(AutoNode): diff --git a/example_auto_nodes/data_node.py b/example_auto_nodes/data_node.py index a5a742d8..c5bf7f8f 100644 --- a/example_auto_nodes/data_node.py +++ b/example_auto_nodes/data_node.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from .node_base.auto_node import AutoNode +from .node_base import AutoNode class VectorSplit(AutoNode): diff --git a/example_auto_nodes/input_nodes.py b/example_auto_nodes/input_nodes.py index 8164188a..fb158574 100644 --- a/example_auto_nodes/input_nodes.py +++ b/example_auto_nodes/input_nodes.py @@ -3,7 +3,7 @@ NODE_PROP_VECTOR3, NODE_PROP_VECTOR4) -from .node_base.auto_node import AutoNode +from .node_base import AutoNode import os diff --git a/example_auto_nodes/logic_nodes.py b/example_auto_nodes/logic_nodes.py index 333e12e1..2701b594 100644 --- a/example_auto_nodes/logic_nodes.py +++ b/example_auto_nodes/logic_nodes.py @@ -1,4 +1,4 @@ -from .basic_nodes import AutoNode +from .node_base import AutoNode class IfNode(AutoNode): diff --git a/example_auto_nodes/module_nodes.py b/example_auto_nodes/module_nodes.py index ce9994e3..b0c79298 100644 --- a/example_auto_nodes/module_nodes.py +++ b/example_auto_nodes/module_nodes.py @@ -1,6 +1,6 @@ -from .node_base.module_node import (ModuleNode, - get_functions_from_module, - get_functions_from_type) +from .node_base import (ModuleNode, + get_functions_from_module, + get_functions_from_type) import os import sys @@ -113,7 +113,8 @@ def is_function(self, obj): return True return False - def get_numpy_args(self, func): + @staticmethod + def get_numpy_args(func): args = [] info = numpydoc.docscrape.FunctionDoc(func) for i in info["Parameters"]: @@ -122,7 +123,7 @@ def get_numpy_args(self, func): args.append(param.split("'")[1]) return args - def addFunction(self, prop, func): + def add_function(self, prop, func): """ Create inputs based on functions arguments. """ @@ -156,7 +157,6 @@ class StringFunctionsNode(ModuleNode): module_functions = get_functions_from_type(_str) - def __init__(self): super(StringFunctionsNode, self).__init__() diff --git a/example_auto_nodes/node_base/__init__.py b/example_auto_nodes/node_base/__init__.py index e69de29b..d67fb1a6 100644 --- a/example_auto_nodes/node_base/__init__.py +++ b/example_auto_nodes/node_base/__init__.py @@ -0,0 +1,3 @@ +from .auto_node import AutoNode +from .module_node import ModuleNode, get_functions_from_module, get_functions_from_type +from .subgraph_node import SubGraphNode, SubGraphInputNode, SubGraphOutputNode, RootNode diff --git a/example_auto_nodes/node_base/auto_node.py b/example_auto_nodes/node_base/auto_node.py index 3e018fff..c9c96d89 100644 --- a/example_auto_nodes/node_base/auto_node.py +++ b/example_auto_nodes/node_base/auto_node.py @@ -131,6 +131,12 @@ def get_data(self, port): Returns: node data. """ + if self.disabled() and self.input_ports(): + out_ports = self.output_ports() + if port in out_ports: + idx = out_ports.index(port) + max_idx = max(0, len(self.input_ports()) - 1) + return self.get_input_data(min(idx, max_idx)) return self.get_property(port.name()) @@ -156,15 +162,6 @@ def get_input_data(self, port): data = from_port.node().get_data(from_port) return copy.deepcopy(data) - def when_disabled(self): - """ - Node evaluation logic when node has been disabled. - """ - - num = max(0, len(self.input_ports())-1) - for index, out_port in enumerate(self.output_ports()): - self.model.set_property(out_port.name(), self.get_input_data(min(index, num))) - def cook(self): """ The entry of the node evaluation. diff --git a/example_auto_nodes/node_base/module_node.py b/example_auto_nodes/node_base/module_node.py index fd140d51..a57a62df 100644 --- a/example_auto_nodes/node_base/module_node.py +++ b/example_auto_nodes/node_base/module_node.py @@ -9,7 +9,7 @@ def _get_functions_from_module(module, function_dict, max_depth=1, module_name=N module_name = module.__name__ for func in funcs: - if func in ["sys","os"]: + if func in ["sys", "os"]: continue new_module_name = module_name + "." + func @@ -44,27 +44,28 @@ class ModuleNode(AutoNode): module_functions = {} - def __init__(self,defaultInputType=None,defaultOutputType=None): - super(ModuleNode, self).__init__(defaultInputType,defaultOutputType) + def __init__(self, defaultInputType=None, defaultOutputType=None): + super(ModuleNode, self).__init__(defaultInputType, defaultOutputType) self.add_combo_menu('funcs', 'Functions', items=list(self.module_functions.keys())) + self.set_dynamic_port(True) # switch math function type - self.view.widgets['funcs'].value_changed.connect(self.addFunction) + self.view.widgets['funcs'].value_changed.connect(self.add_function) self.add_output('output') self.create_property('output', None) self.view.widgets['funcs'].widget.setCurrentIndex(0) - self.addFunction(None, self.view.widgets['funcs'].widget.currentText()) + self.add_function(None, self.view.widgets['funcs'].widget.currentText()) - def is_function(self,obj): + def is_function(self, obj): if inspect.isfunction(self.func) or inspect.isbuiltin(self.func): return True - elif "method" in type(obj).__name__ or "function" in type(obj).__name__: + elif "method" in type(obj).__name__ or "function" in type(obj).__name__: return True return False - def addFunction(self, prop, func): + def add_function(self, prop, func): """ Create inputs based on functions arguments. """ @@ -80,17 +81,14 @@ def addFunction(self, prop, func): self.process_args(args) - def process_args(self,in_args, out_args = None): + def process_args(self, in_args, out_args=None): for arg in in_args: if arg not in self.inputs().keys(): self.add_input(arg) for inPort in self.input_ports(): - if inPort.name() in in_args: - if not inPort.visible(): - inPort.set_visible(True) - else: - inPort.set_visible(False) + if inPort.name() not in in_args: + self.delete_input(inPort) if out_args is None: return @@ -100,11 +98,8 @@ def process_args(self,in_args, out_args = None): self.add_output(arg) for outPort in self.output_ports(): - if outPort.name() in out_args: - if not outPort.visible(): - outPort.set_visible(True) - else: - outPort.set_visible(False) + if outPort.name() not in out_args: + self.delete_output(outPort) def run(self): """ diff --git a/example_auto_nodes/node_base/subgraph_node.py b/example_auto_nodes/node_base/subgraph_node.py index 6144fd7d..7f8f1c40 100644 --- a/example_auto_nodes/node_base/subgraph_node.py +++ b/example_auto_nodes/node_base/subgraph_node.py @@ -1,4 +1,5 @@ from .auto_node import AutoNode +from .utils import update_node_down_stream from NodeGraphQt import SubGraph import json from NodeGraphQt import topological_sort_by_down, BackdropNode @@ -18,11 +19,11 @@ def __init__(self, defaultInputType=None, defaultOutputType=None, dynamic_port=T self.create_property('graph_rect', None) self.create_property('published', False) if dynamic_port: - self.model.dynamic_port = True + self.set_dynamic_port(True) self.add_int_input('input count', 'input count', 0) self.add_int_input('output count', 'output count', 0) else: - self.model.dynamic_port = False + self.set_dynamic_port(False) self.create_property('input count', 0) self.create_property('output count', 0) self._marked_ports = [] @@ -165,9 +166,8 @@ def run(self): for node in nodes: if node.disabled(): - node.when_disabled() - else: - node.cook() + continue + node.cook() if node.has_error: self.error("/"+node.view.toolTip()) break @@ -413,6 +413,22 @@ def get_data(self, port=None): for from_port in from_ports: return from_port.node().get_data(from_port) + def run(self): + parent = self.parent() + if parent is None or not parent.auto_cook: + return + + port = parent.get_output(self.get_property('output index')) + if not port: + return + + to_ports = port.connected_ports() + if not to_ports: + return + + nodes = [p.node() for p in to_ports] + update_node_down_stream(nodes) + class RootNode(SubGraphNode): """ @@ -439,7 +455,6 @@ def cook(self): def run(self): pass - def error(self, message=None): - if message is None: - return False - self._error = False \ No newline at end of file + @property + def has_error(self): + return False \ No newline at end of file diff --git a/example_auto_nodes/node_base/utils.py b/example_auto_nodes/node_base/utils.py index a930085a..81496a15 100644 --- a/example_auto_nodes/node_base/utils.py +++ b/example_auto_nodes/node_base/utils.py @@ -1,19 +1,68 @@ -from NodeGraphQt import topological_sort_by_down +from NodeGraphQt import topological_sort_by_down, NodePublishWidget + +# node stream update def _update_nodes(nodes): for node in nodes: if node.disabled(): - node.when_disabled() - else: - node.cook() + continue + node.cook() if node.has_error: break -def update_node_down_stream(node): - _update_nodes(topological_sort_by_down(start_nodes=[node])) +def update_node_down_stream(nodes): + if not isinstance(nodes, list): + nodes = [nodes] + _update_nodes(topological_sort_by_down(start_nodes=nodes)) def update_nodes(nodes): _update_nodes(topological_sort_by_down(all_nodes=nodes)) + + +# node menu + +def setup_node_menu(graph, published_node_class): + from .auto_node import AutoNode + from .subgraph_node import SubGraphNode + + node_menu = graph.context_nodes_menu() + node_menu.add_command('Allow Edit', allow_edit, node_class=published_node_class) + node_menu.add_command('Enter Node', enter_node, node_class=SubGraphNode) + node_menu.add_command('Publish Node', publish_node, node_class=SubGraphNode) + node_menu.add_command('Print Children', print_children, node_class=SubGraphNode) + node_menu.add_command('Cook Node', cook_node, node_class=AutoNode) + node_menu.add_command('Toggle Auto Cook', toggle_auto_cook, node_class=AutoNode) + node_menu.add_command('Print Path', print_path, node_class=AutoNode) + + +def cook_node(graph, node): + node.update_stream(forceCook=True) + + +def toggle_auto_cook(graph, node): + node.auto_cook = not node.auto_cook + + +def enter_node(graph, node): + graph.set_node_space(node) + + +def allow_edit(graph, node): + node.set_property('published', False) + + +def print_path(graph, node): + print(node.path()) + + +def print_children(graph, node): + children = node.children() + print(len(children), children) + + +def publish_node(graph, node): + wid = NodePublishWidget(node=node) + wid.show() diff --git a/example_auto_nodes/subgraph_nodes.py b/example_auto_nodes/subgraph_nodes.py index 53390464..24659fc1 100644 --- a/example_auto_nodes/subgraph_nodes.py +++ b/example_auto_nodes/subgraph_nodes.py @@ -1,4 +1,4 @@ -from .node_base.subgraph_node import SubGraphNode, SubGraphInputNode, SubGraphOutputNode +from .node_base import SubGraphNode, SubGraphInputNode, SubGraphOutputNode import json import os diff --git a/example_auto_nodes/viewer_nodes.py b/example_auto_nodes/viewer_nodes.py index 7ce95579..74c71a82 100644 --- a/example_auto_nodes/viewer_nodes.py +++ b/example_auto_nodes/viewer_nodes.py @@ -1,4 +1,4 @@ -from .node_base.auto_node import AutoNode +from .node_base import AutoNode class DataViewerNode(AutoNode): diff --git a/example_auto_nodes/widget_nodes.py b/example_auto_nodes/widget_nodes.py index ff5f90a7..17beab6a 100644 --- a/example_auto_nodes/widget_nodes.py +++ b/example_auto_nodes/widget_nodes.py @@ -1,4 +1,4 @@ -from .node_base.auto_node import AutoNode +from .node_base import AutoNode class DropdownMenuNode(AutoNode):