.. _node-documentation: Node Documentation ================== The intention of this library is to make the creation of Nodes easy. You do not need to know how to program GUIs or know Qt (PyQt4 or PySide), although it is useful to apply advanced Node interactions. Basic Construction of a Node ---------------------------- A node consists of a collection of classes including the top-level :class: nodeworks.node.Node, the NodeGraphic (which handles the drawing of the node), and an arbitrary number of Terminals. The Terminals contain the Pads for connections as well as a widget, if specified. To create a custom Node, create a Python class, inherit from the Node class, and give the node a name: .. code-block:: python from nodeworks import Node class MyCustomNode(Node): name = 'my node' Now we need to initialize the node by defining the terminal options and then calling the ``__init__`` method of the Node class: .. code-block:: python def __init__(self, parent=None): self.terminalOpts = {} Node.__init__(self, parent) We now have a very simple node. To make the node actually perform an action when the Node Network is processed we need to overwrite the process method: .. code-block:: python def process(self): print(self.name) Putting the above code together we have: .. code-block:: python from nodeworks import Node class MyCustomNode(Node): name = 'my node' def __init__(self, parent=None): self.terminalOpts = {} Node.__init__(self, parent) def process(self): print(self.name) This is the most basic node that can be produced, it does not have any Terminals and when run, simply prints its name. See the Terminal section below on how to create Terminals Terminals --------- Terminals are defined by a dictionary containing options for the terminals. The options define the names of the terminals, whether the terminal is an input or output, if the terminal has a widget and what that widget is, as well as others. Building on the code in the Basic Construction of a Node section, we will define the ``self.terminalOpts`` dictionary. The entries of this dictionary define the terminal names as well as the options of each terminal. As an example, we will create a terminal named *input*: .. code-block:: python self.terminalOpts = {'input':{}, } In python, the order of entries in a dictionary are not preserved. Due to this, the order of the Terminals may change and not reflect the order that they are defined. This is fine for simple Nodes. However, Python's built-in OrderedDict can be used to preserve the order: .. code-block:: python from collections import OrderedDict self.terminalOpts = OrderedDict([('input',{}), ]) This is the simplest terminal. Since we defined an empty dictionary as the value of the dictionary entry, the defaults will be used. Accessing/Setting Terminal Values +++++++++++++++++++++++++++++++++ To access the terminal throughout the Node methods, the ``self.terminals`` attribute needs to be indexed. This can be accomplished one of two ways. If we have a terminal named ``input`` as defined in the ``self.terminalOpts`` attribute, we can access the terminal through indexing: .. code-block:: python self.terminals['input'] or through an attribute: .. code-block:: python self.terminals.input *Note: if their are spaces in the terminal name (like ``input int``), then only the indexing method works. Otherwise, both methods will work.* Once the terminal is accessed, the current value can be obtained by using the ``value`` property: .. code-block:: python input = self.terminals.input.value and the value can be set by calling the ``setValue(value)`` method: .. code-block:: python self.terminals.input.setValue(10) Terminal Options +++++++++++++++++ Now we can start customizing the terminal by populating the terminal dictionary with values such as: .. code-block:: python self.terminalOpts = {'input':{'widget':'lineedit', 'in':True, 'out':False, 'showlabel':True, 'multiinput':True, 'dtype':str, 'value':'enter text here' }, } Accepted keys +-------------------+----------------------------------------+-----------------+ | Key | Function | type | +===================+========================================+=================+ | widget | sets the widget that is displayed | str | +-------------------+----------------------------------------+-----------------+ | in | shows input pad | bool | +-------------------+----------------------------------------+-----------------+ | out | shows output pad | bool | +-------------------+----------------------------------------+-----------------+ | min | sets a minimum value | int/float | +-------------------+----------------------------------------+-----------------+ | max | sets a maximum value | int/float | +-------------------+----------------------------------------+-----------------+ | showlabel | show/hide terminal label | bool | +-------------------+----------------------------------------+-----------------+ | multiinput | accept multiple inputs | bool | +-------------------+----------------------------------------+-----------------+ | checkable | makes the widget checkable | bool | +-------------------+----------------------------------------+-----------------+ | dtype | sets the dtype of the terminal | type | +-------------------+----------------------------------------+-----------------+ | value | the initial value of the terminal | anything | +-------------------+----------------------------------------+-----------------+ | items | list of items | list | +-------------------+----------------------------------------+-----------------+ | buttons | list of button names | list of strs | +-------------------+----------------------------------------+-----------------+ | columns | list of column names | list of strs | +-------------------+----------------------------------------+-----------------+ | columedelegate | dictionary describing column delegates | dict | +-------------------+----------------------------------------+-----------------+ | rows | list of row names | list of strs | +-------------------+----------------------------------------+-----------------+ | rowdelegate | dictionary describing row delegates | dict | +-------------------+----------------------------------------+-----------------+ | selectionBehavior | sets the selection behavior of a table | str | +-------------------+----------------------------------------+-----------------+ | selectionMode | sets the selection mode of a table | str | +-------------------+----------------------------------------+-----------------+ | browsetype | sets the dialog type | str | +-------------------+----------------------------------------+-----------------+ *Note: not accepted keys fail silently* Widgets +++++++ lineedit ******** .. figure:: ./images/lineedit.png :align: center :figclass: align-center The ``lineedit`` widget provides a QLineEdit in the Terminal that a user can enter text into. This text will be converted to the type that is specified with the ``dtype`` option. For example, the following options creates a line edit that will convert the entered text into a float: .. code-block:: python self.terminalOpts = {'input':{'widget':'lineedit', 'in':True, 'out':False, 'showlabel':True, 'dtype':float, }, } *Note: ``float('text')`` causes an exception.* display ******* .. figure:: ./images/display.png :align: center :figclass: align-center The ``display`` widget provides a disabled ``lineedit`` that can be used to display values on the Node. .. code-block:: python self.terminalOpts = {'input':{'widget':'display', 'in':True, 'out':False, 'showlabel':True, 'dtype':str, }, } checkbox ******** .. figure:: ./images/checkbox.png :align: center :figclass: align-center The ``checkbox`` widget provides a QCheckBox in the Terminal that allows users to toggle True or False. The ``dtype`` should be generally ``bool``, however, other types are acceptable as well such as ``str`` because ``str(True)`` is valid. .. code-block:: python self.terminalOpts = {'input':{'widget':'checkbox', 'in':True, 'out':False, 'showlabel':True, 'dtype':bool, }, } combobox ******** .. figure:: ./images/combobox.png :align: center :figclass: align-center The ``combobox`` widget provides a QComboBox in the Terminal that allows users to select an item from a pre-defined list. This list is defined with the ``items`` option. *Note: The items in the list must be strings* .. code-block:: python self.terminalOpts = {'input':{'widget':'combobox', 'in':True, 'out':False, 'showlabel':True, 'dtype':str, 'items':['one', 'Two', 'False', '100'], }, } spinbox/doublspinbox ******************** .. figure:: ./images/spinbox.png :align: center :figclass: align-center The ``spinbox`` and ``doublespinbox`` widgets provide ``int`` and ``float`` user inputs, respectively. The ``dtype`` option should be set accordingly. The ``min`` and ``max`` options set the minimum and maximum values that can be entered. .. code-block:: python self.terminalOpts = {'input':{'widget':'spinbox', 'in':True, 'out':False, 'showlabel':True, 'dtype':int, 'min':-1, 'max':10, }, } browse ****** .. figure:: ./images/browse.png :align: center :figclass: align-center The ``browse`` widget provides both a lineedit to show/edit the path as well as a button that opens a file dialog. The ``dtype`` option should be a ``str``. The ``browsetype`` option can be either ``open``, ``save``, or ``directory``, setting the dialog type to ``QFileDialog.getOpenFileName``, ``QFileDialog.getSaveFileName``, or ``QFileDialog.getExistingDirectory``, respectively. .. code-block:: python self.terminalOpts = {'input':{'widget':'browse', 'in':True, 'out':False, 'showlabel':True, 'dtype':str, 'browstype':'open', }, } pushbutton/toolbutton ********************* .. figure:: ./images/pushbutton.png :align: center :figclass: align-center The ``pushbutton`` and ``toolbutton`` widgets provide ``QPushButton`` and ``QToolButton`` terminal inputs, respectively. The only difference between these buttons is that the ``pushbutton`` is raised and the ``toolbutton`` autorasies when the mouse is hovered over the button. These buttons can be checkable by setting the ``checkable`` option to ``True``. .. code-block:: python self.terminalOpts = {'input':{'widget':'pushbutton', 'in':True, 'out':False, 'showlabel':False, 'dtype':bool, 'checkable':True, }, } Icons can be applied to the buttons using a ``*.png`` file by using a helper function provided in the ``tools`` module. This is typical set in the ``__init__`` method of the ``Node``: .. code-block:: python from nodeworks import Node, tools class MyCustomNode(Node): name = 'my node' def __init__(self, parent=None): self.terminalOpts = {'input':{'widget':'toolbutton', 'in':True, 'out':False, 'showlabel':False, 'dtype':bool, 'checkable':True, }, } Node.__init__(self, parent) self.terminals['input'].widget.setIcon(tools.getIcon('addbox.png')) See the :download:`examples/buttonnode.py <../../../nodeworks/examples/nodes/buttonnode.py>` for a complete example. toolbuttons/pushbuttons *********************** .. figure:: ./images/pushbuttons.png :align: center :figclass: align-center The ``toolbuttons`` and ``pushbuttons`` widgets are simply a list of ``toolbutton`` and ``pushbutton``, respectively. The number of buttons and their names are defined by setting the ``buttons`` option to a list of strings. for example, the following creates a terminal with three buttons: .. code-block:: python self.terminalOpts = {'input':{'widget':'pushbuttons', 'in':False, 'out':False, 'showlabel':False, 'dtype':list, 'checkable':True, 'buttons':['push me', 'btn', 'stop'], }, } The individual buttons can be accessed through the index notation: .. code-block:: python self.terminals['input'].widget['push me'] or through attributes .. code-block:: python self.terminals.input.widget.stop or both .. code-block:: python self.terminals['input'].widget.btn See the :download:`examples/multibuttonnode.py <../../../nodeworks/examples/nodes/multibuttonnode.py>` for a complete example. textedit ******** .. figure:: ./images/textedit.png :align: center :figclass: align-center The ``textedit`` widget provides a ``QTextEdit`` terminal input. This allows for multiline text input. .. code-block:: python self.terminalOpts = {'input':{'widget':'textedit', 'in':False, 'out':True, 'showlabel':False, 'dtype':str, 'value':'\n'.join(['multi', 'line', 'text', 'input']) }, } table ***** .. figure:: ./images/table.png :align: center :figclass: align-center The ``table`` widget provides a ``QtWidgets.QTableView`` with two different ``QAbstractTableModel`` as well as a custom ``QStyledItemDelegate``. The table can work with ``dict(dict())``, ``[]``, ``[[]]``, ``()``, ``(())``, ``[()]``, ``([])``, 2D ``numpy.ndarray``, and ``pandas.DataFrame`` objects. Delegates can be applied column wise as well as row wise, however the columns take precedence. Simple example without column or row delegates: .. code-block:: python self.terminalsOpts = {'table':{'widget':'table', 'in':False, 'out':True, 'dtype':list, 'multiinput':False, 'columndelegate':{}, 'rowdelegate':{}, 'columns':None, 'rows':None, 'value':[[1.1,2,3,10.0, True], [5,6,7,8, False]] } } Columns and rows can be resized to fit the contents of the cells by calling the ``fitToContents`` method of the widget: .. code-block:: python self.terminals['table'].widget.fitToContents() Column and row delegates are created by passing dictionaries to the ``columndelegate`` and ``rowdelegate`` options, respectively. This dictionary contains other dictionaries that describe the delegates to use. For example, the following dictionary locks the first row/column, sets a ``combobox`` for the next row/column, a ``spinbox`` for the third, a ``doublespinbox`` for the fourth, and a ``checkbox`` for the fifth. .. code-block:: python {0:{'widget':None}, 1:{'widget':'combobox', 'items':[str(i) for i in range(100)], 'dtype': str}, 2:{'widget':'spinbox', 'dtype': int, 'min':-10, 'max':10, }, 3:{'widget':'doublespinbox', 'dtype': float, 'min':-1, 'max':1, }, 4:{'widget':'checkbox', 'dtype': bool, } } The selection behavior and selection mode can also be set using the ``selectionBehavior`` and ``selectionMode`` keys. ``selectionBehavior`` can either be ``row``, ``col``, or ``cell`` to select row selection, columns selection or individual cell selections, respectivly. The ``selectionMode`` can be set to either ``single`` or ``multi`` for single selections or multiple selections. Whenever the selection changes, the ``newSelection`` signal is emitted. The signal contains the from and to positions of the selection as ``[row, col]``. .. code-block:: python from nodeworks import Node, tools class MyCustomNode(Node): name = 'my node' def __init__(self, parent=None): self.terminalsOpts = {'table':{'widget':'table', 'in':False, 'out':True, 'dtype':list, 'multiinput':False, 'columndelegate':{}, 'rowdelegate':{}, 'columns':None, 'rows':None, 'value':[[1.1,2,3,10.0, True], [5,6,7,8, False]], 'selectionBehavior':'row', 'selectionMode':'multi', } } Node.__init__(self, parent) self.terminals['table'].widget.newSelection.connect(self.newSelection) def newSelection(self, from_, to): print('selection from:', from_, 'to', to) see the :download:`examples/tablenode.py <../../../nodeworks/examples/nodes/tablenode.py>` for a complete example. Connecting Signals ------------------ Sometimes it is useful to make Nodes that adapt to user interaction. To make this work, we have to use Qt's signals and slots. Every widget has a ``valueChanged`` signal that is emitted every time a widget's value is changed. We can use this signal to call specific methods of the Node. For example, we can add a ``button`` terminal to a node and ``connect`` the signal to a method that ``prints`` the name of the node: .. code-block:: python class MyCustomNode(Node): name = 'my node' def __init__(self, parent=None): self.terminalOpts = {'btn':{'widget':'pushbutton', 'in':False, 'out':False, 'dtype':bool, 'showlabel':False, } } Node.__init__(self) self.terminals['btn'].valueChanged.connect(self.printMyName) def printMyName(self): print(self.name) see the :download:`examples/dynamicnode.py <../../../nodeworks/examples/nodes/dynamicnode.py>` for a complete example. Accessing the QObject --------------------- Since these widgets are Qt objects, all the methods of the object can be applied such as ``setStyeSheet`` to change the formatting of a widget. The widgets can be accessed by the ``widget`` attribute: .. code-block:: python self.terminals['input'].widget Error Handling -------------- Primitive error checking/handling is provided so that a `Node` can interrupt the the solving of the flow sheet. This can be accomplished two ways. The first way is to check for errors before the `process` method of the `Node` is called. This is accomplished by overriding the `errorCheck` method of the `Node`: .. code-block:: python class MyCustomNode(Node): name = 'my node' def __init__(self, parent=None): self.terminalOpts = {} Node.__init__(self) def errorCheck(self): ''' Check for errors ''' error = True if error: self.setError('Error message') The second option is to call the `setError` method anywhere in the `Node` including the `process` method. For example: .. code-block:: python class MyCustomNode(Node): name = 'my node' def __init__(self, parent=None): self.terminalOpts = {} Node.__init__(self) def process(self): try: result = 10/0.0 except ZeroDivisionError: self.setError("Can't divide by zero") It is also possible to clear an error by calling the `clearError` method. Node Documentation ------------------ To provide help and information about a `Node`, a docstring can be added to the `Node`. There are three possible `Terminal` categories including input terminals, output terminals, and user terminals (no output or input): .. code-block:: python class MyCustomNode(Node): ''' My node that does something awesome. Input Terminals --------------- name (str): your name Output Terminals ---------------- age (int): a guess of your age User Terminals -------------- gender (str): your gender ''' name = 'my node' def __init__(self, parent=None): self.terminalOpts = {} Node.__init__(self)