# -*- coding: utf-8 -*-
"""
This file is part of the nodeworks core library
License
-------
As a work of the United States Government, this project is in the public domain
within the United States. As such, this code is licensed under
CC0 1.0 Universal public domain.
Please see the LICENSE.md for more information.
"""
import copy
import pickle
import traceback
from typing import Any, Mapping, Optional, Iterable, Set
try:
import numpy as np
except ImportError:
np = None
try:
import pandas as pd
except ImportError:
pd = None
try:
import pygments as pyg
from pyg.lexers import get_lexer_by_name
from pyg.formatters import get_formatter_by_name
except ImportError:
pyg = None
from qtpy import QtWidgets, QtGui, QtCore
from nodeworks.widgets.basewidgets import (
CheckBox, ComboBox, DoubleSpinBox, Label, LineEdit, SpinBox, Title, Display,
Browse, PushButton, ToolButton, PushButtons, ToolButtons, TextEdit, Table,
ComboBoxCompleter, List, ProgressBar)
from nodeworks.tools import (
connection_type_func, triangle_path, TYPEMAP, recurse_dict, set_in_dict,
get_icon, WorkerThread)
def is_title_bar(wid):
'''
Check the widget to see if it is the title bar or title
'''
return (isinstance(wid, Title) or
(isinstance(wid, QtWidgets.QWidget) and wid.objectName() == 'title_widget'))
[docs]class Node(QtCore.QObject):
'''
Main class for a Node.
Parameters
----------
parent (QObject):
parent of the node, typically :class:`nodeworks.nodewidget.NodeWidget`
(default None)
style (dict):
a dictionary describing the style of the node
'''
newConnectionEvent = QtCore.Signal(object, object, object)
newPadEvent = QtCore.Signal(object)
valueChanged = QtCore.Signal(object)
onLoad = QtCore.Signal()
updateTerminalValues = QtCore.Signal()
update_progress = QtCore.Signal(object)
name = 'Node'
terminalOpts: Mapping[str, Mapping[str, Any]] = {}
def __init__(self, parent=None, style={}):
QtCore.QObject.__init__(self)
self.terminals: Mapping[str, Any] = {}
self.locals: Mapping[str, Any] = {}
self.terminalPadList: Iterable[Any] = []
self.style: Mapping[str, Mapping[str, Any]] = {'terminal': {}}
self.style.update(style)
self.error = False
self.running = False
self.runState = 1
self.errorMessage = None
self.stateChanged = True
self.parent = parent
self.uniqueName = self.name
self.path: Iterable[Any] = []
self.forceRun = False
self.hidden = False
self.layer = -1
self.manual_thread_running = False
# Node grapghic object
self.nodeGraphic = NodeGraphic(self, style=self.style)
self.expert_mode_btn = self.nodeGraphic.expert_mode_btn
self.updateTerminalValues.connect(self.nodeGraphic.updateTerminalValues)
self.update_progress.connect(self.nodeGraphic.update_progress)
# build terminals
self._buildTerminals()
def _buildTerminals(self):
'''
Convenience function for building terminals defined in self.terminals.
'''
for k, v in self.terminalOpts.items():
opts = {'name':k}
opts.update(v)
term = self.nodeGraphic.addTerminal(opts, style=self.style['terminal'])
self.terminals[k] = term
self.nodeGraphic.resizeToMinimum()
def _updateTerminalsNodeName(self):
for v in self.terminals.values():
v.nodeName = self.uniqueName
def __getitem__(self, name):
if name in self.terminals:
return self.terminals[name]
else:
raise IndexError('{} is not a terminal'.format(name))
def __getattr__(self, name):
if name in self.terminals:
return self.terminals[name]
else:
raise AttributeError('{} is not an terminal'.format(name))
def __getstate__(self):
clean_dict = {}
for k, v in self.__dict__.items():
if k != 'parent' and k != 'nodeGraphic':
try:
pickle.dumps(v, protocol=-1)
clean_dict[k] = v
except Exception as e:
pass
return clean_dict
def __setstate__(self, state):
self.__dict__ = state
def keyPressEvent(self, event):
pass
[docs] def terminalChanged(self, value):
'''
A function that is called if a value is changed. This enables the node
for processing
Parameters
----------
value (Object):
the value to emit with the valueChanged signal
'''
self.valueChanged.emit(value)
self.stateChanged = True
[docs] def setUniqueName(self, name):
"""
Set the unique name of the node. This name is used in
:attr:`NodeWidget.nodeDict`.
Parameters
----------
name (str):
the unique name
"""
self.uniqueName = name
self._updateTerminalsNodeName()
[docs] def setStyle(self, style=None):
'''
Set the style of the `Node`, as well as the terminals
Parameters
----------
style (dict):
a dictionary describing the style
'''
if isinstance(style, dict):
self.style.update(style)
for term in self.terminals.values():
term.setStyle(self.style['terminal'])
self.nodeGraphic.setStyle(self.style.get('node', {}))
def setPath(self, path):
self.path = path
[docs] def removeConnections(self):
'''
Remove all connections from the `Node`.
'''
for term in self.terminals.values():
term.removeAllConnections()
[docs] def addTerminal(self, name, opts={}, insert=None):
'''
Add a terminal to the Node.
Parameters
----------
name (str):
the name of the :class:`Terminal`, and the index of the
:class:`Terminal` in :attr:`terminals`
opts (dict):
a dictionary of :class:`Terminal` options (default dict())
insert (int):
the position to insert the `Terminal` at.
Returns
-------
term (Terminal):
the created terminal
'''
opts.update({'name':name})
term = self.nodeGraphic.addTerminal(opts,
style=self.style['terminal'],
insert=insert)
self.terminals[name] = term
self.stateChanged = True
return term
def _addPad(self, pad):
self.terminals[pad.name] = pad
self.nodeGraphic.terminalDict[pad.name] = pad
self.newPadEvent.emit(pad)
[docs] def removeTerminal(self, name):
'''
Remove the terminal from the Node.
Parameters
----------
name (str):
name of the terminal to be removed
'''
if name not in self.terminals:
return
term = self.terminals.pop(name)
self.nodeGraphic.removeTerminal(term)
self.stateChanged = True
[docs] def removeAllTerminals(self, inout=None):
'''
Remove all terminals from the Node.
'''
remove = []
for k in self.terminals:
if self.nodeGraphic.terminalDict[k].opts[inout]:
remove.append(k)
for k in remove:
self.removeTerminal(k)
def getListOfInputs(self):
names = []
for k in self.terminals:
if self.nodeGraphic.terminalDict[k].opts['in']:
names.append(k)
return names
[docs] def dependencies(self):
'''
Collect and return the dependencies of the `Node`
Returns
-------
depSet (set):
a set nodes that this node depends on
'''
depSet: Set[Any] = set()
for term in self.terminals.values():
dep = term.dependency()
if dep is not None:
depSet = depSet.union(dep)
return depSet
[docs] def depsFinished(self):
'''
Have the dependencies of the `Node` been processed
Returns
-------
(bool):
are the dependencies finished
'''
runlist = []
deps = self.dependencies()
for dep in deps:
runlist.append(self.parent.nodeDict[dep].runState)
# 0 is processed, 1 is running, 2 is errant
return (max(runlist) if runlist else
0)
[docs] def setError(self, message='None', terminal=None):
'''
Set the `Node` in an error state, will not let `Node` execute.
Parameters
----------
message (str):
an error message to be displayed for user information
terminal (str):
name of the terminal to highlight in red
'''
self.error = True
self.errorMessage = message
# change the error icon red
if self.nodeGraphic.show_error_btn:
self.nodeGraphic.show_error_btn.setIcon(get_icon('warning_red'))
self.nodeGraphic.show_error_btn.setEnabled(True)
# if terminal, set text red
if terminal is not None:
term = self.terminals.get(terminal)
if term is not None and term.label is not None:
term.label.setStyleSheet("QLabel{color:red}")
for conn in term.inputConnection:
conn.setError()
# self.nodeGraphic.update(self.nodeGraphic.rect())
[docs] def clearError(self):
'''
Clear the `Node`'s error state, allow to run.
'''
self.error = False
self.errorMessage = None
# change the error icon back to black
if self.nodeGraphic.show_error_btn:
self.nodeGraphic.show_error_btn.setIcon(get_icon('warning'))
self.nodeGraphic.show_error_btn.setEnabled(False)
for term in self.terminals.values():
if term.label is not None:
term.label.setStyleSheet("")
for conn in term.inputConnection:
conn.clearError()
# self.nodeGraphic.update(self.nodeGraphic.rect())
[docs] def setRunning(self):
'''
Set the `Node` is running.
'''
self.running = True
self.nodeGraphic.update(self.nodeGraphic.rect())
[docs] def clearRunning(self):
'''
Set the `Node` is not running.
'''
self.running = False
self.update_progress.emit(None)
self.nodeGraphic.update(self.nodeGraphic.rect())
[docs] def errorCheck(self):
'''
Override this function with code to check for errors.
'''
pass
[docs] def runNode(self):
'''
Run the node. This function updates connections, clears any errors,
checks for errors, finally if there are no errors and the input values
have changed, calls the `process` method
Returns
-------
ran (bool):
`False` if `process` was not called (error) else `True`
'''
# Update inputs
self.updateInputs()
# check for errors before calling process
self.clearError()
self.errorCheck()
if self.error:
return False
# if the current state has changed, process node
if self.stateChanged or self.forceRun:
# set runState to running
self.runState = 1
try:
self.process()
except:
self.error = True
e = traceback.format_exc()
self.errorMessage = 'Node failed to execute. Traceback:\n{}'.format(e)
print('{} failed to execute. Traceback:\n{}'.format(self.uniqueName, e))
# check for error after calling process, i.e. error checking in process
if self.error:
# set runState to error
self.runState = 2
return False
self.stateChanged = False
# set runState to finished
self.runState = 0
return True
[docs] def manual_process(self):
'''
Call this method to run the self.process method threaded.
'''
if self.manual_thread_running:
return
self.manual_thread_running = True
self.nodeGraphic.w.setEnabled(False)
self.clearError()
# manually pull updated model
self.updateInputs()
thread = self.manual_thread = WorkerThread(self.process)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(self.manual_process_finished)
thread.start()
def manual_process_finished(self):
self.manual_thread_running = False
self.stateChanged = False
self.nodeGraphic.w.setEnabled(True)
[docs] def process(self):
'''
Override this function with the code to be called when the Node is
executed.
'''
pass
[docs] def state(self, customParams=None):
'''
The current state of the node (position, values, connections, path)
Returns
-------
stateDict (dict):
a JSON encodable dictionary
'''
stateDict = {
'type': 'Node',
'name': self.name,
'uniquename': self.uniqueName,
'pos': [self.nodeGraphic.pos().x(),
self.nodeGraphic.pos().y()],
'path': self.path,
'terminals': {},
'tunnel': False,
'forcerun': self.forceRun,
'hidden': self.hidden,
'customState': {},
'layer': self.layer}
if self.nodeGraphic.manualResize:
stateDict['geometry'] = [self.nodeGraphic.geometry().width(),
self.nodeGraphic.geometry().height()]
# Collect terminal Data
for term in self.terminals.values():
# collect terminal value
if term.widget and term.widget.value is not None and \
term.dtype not in [(np.ndarray if np else None),
(pd.DataFrame if pd else None)]:
try:
value = term.dtype(term.widget.value)
except ValueError:
value = None
else:
value = None
# save if terminal is flipped
if hasattr(term, 'flipped'):
flipped = term.flipped
else:
flipped = False
# save terminal options. Needed for structures
termopts = copy.deepcopy(term.opts)
#covert type objects to text
for compkey, v in recurse_dict(termopts):
if compkey[-1] == 'dtype':
set_in_dict(termopts, compkey, str(v))
stateDict['terminals'][term.opts['name']] = {
'value': value,
'flipped': flipped,
'opts': termopts,
}
# if the widget is a structure, save the items in the structure
if hasattr(self, 'nodeWidget'):
# build save dictionary
saveDict = {}
for key, node in self.nodeWidget.nodeDict.items():
saveDict[key] = node.state()
stateDict['nodewidget'] = saveDict
stateDict['customState'] = self.setCustomState(customParams=customParams)
#if the widget is a case node
if hasattr(self, 'nodeWidgets'):
for i in range(0, self.switch.count()):
# build save dictionary
saveDict = {}
#for key, node in self.nodeWidgets[i].nodeDict.items():
# saveDict[key] = node.state()
for key, node in self.switch.widget(i).nodeDict.items():
saveDict[key] = node.state()
stateDict['nodewidget_'+str(i)] = saveDict
return stateDict
[docs] def setState(self, state, customParams=None):
'''
Set the state. The uniquename, position, and connections are set
through the `addNode` method of `NodeWidget`.
Parameters
----------
state (dict):
a dictionary containing information to set the terminal values
'''
# save a reference for nodes that need this information for backward
# compatibility
self.loaded_state = state
# convert strings back to dtypes
for compkey, value in recurse_dict(state):
if compkey[-1] == 'dtype':
if isinstance(value, list):
value = value[0]
if isinstance(value, str):
try:
value = TYPEMAP[value]
except:
pass
set_in_dict(state, compkey, value)
# set the name
self.nodeGraphic.rename(state.get('name', 'Node'))
if 'terminals' in state:
terms = state.get('terminals', {})
# add in sorted order
# sorted cannot handle ordering of lists that contain numbers and
# strings - 1,2,6,8,9,10,test becomes 1,10,2,6,7,8,9,test
intList = []
strList = []
for key in terms.keys():
# try to cast the terminal key to an int
try:
intList.append(int(key))
# if that fails add it as a string
except:
strList.append(key)
# sort the integers
intList = sorted(intList)
# convert the ints back to strings
intList_s = [str(x) for x in intList]
# sort the strings
strList = sorted(strList)
# concatenate the int and string lists
sortedTerms = intList_s + strList
for name in sortedTerms:
term = terms.get(name, {})
term_opts = term.get('opts', {})
# there is an entry for a terminal that does not exist
if name not in self.terminals:
self.addMissingTerminal(term)
# set values for the normal pads
if name in self.terminals:
self.terminals[name].dtype = term_opts.get('dtype', None)
self.terminals[name].setValue(term['value'])
# set flipped state
if hasattr(self.terminals[name], 'flipped') and 'flipped' in term:
self.terminals[name].flipped = term['flipped']
else:
self.terminals[name].flipped = False
# populate terminal pads
elif 'termpad' in term_opts and term_opts['termpad']:
# create a new termianl pad
newterm = self.nodeGraphic.addTerminalPad(
connect=False,
rect=QtCore.QRectF(*term_opts['rect']),
name=term_opts['name'],
inout=term_opts['inout'],
dtype=term_opts['dtype'],
padtype=term_opts['padtype'],
load=True
)
# check to see if it is a shift pad
if term_opts['padtype'] == 'shift':
# find and connect shift pads
for exterm in self.terminalPadList:
if exterm.name == term_opts['shiftpad']:
newterm.shiftPad = exterm
exterm.shiftPad = newterm
if 'geometry' in state:
self.nodeGraphic.manualResize = True
QtWidgets.QApplication.instance().processEvents() #XXX is this needed?
self.nodeGraphic.resize(QtCore.QSizeF(*state['geometry']))
else:
self.nodeGraphic.resizeToMinimum()
self.forceRun = state.get('forcerun', False)
if self.nodeGraphic.force_run_btn is not None:
self.nodeGraphic.force_run_btn.setChecked(self.forceRun)
self.hidden = state.get('hidden', False)
self.setLayer(state.get('layer', 0))
if self.layer > self.nodeGraphic.scene().topLayer:
self.nodeGraphic.scene().topLayer = self.layer
self.getCustomState(state.get('customState', {}), customParams=customParams)
# update the terminal widgets
for name, term in self.terminals.items():
term.updateWidget()
[docs] def addMissingTerminal(self, terminal):
'''
This method is called when options for a terminal exist in the state
however, the terminal does not exist in the `Node`. This method is
meant to be overridden.
Parameters
----------
terminal (dict):
a dictionary of options
'''
pass
[docs] def collapse(self, hide=None):
'''
Collapse/expand the node down to only the connected terminals.
Parameters
----------
hide (bool):
a boolean that determines if the terminals should be hidden
'''
if hide is None:
self.hidden = not self.hidden
else:
self.hidden = hide
hide = self.hidden
for term in self.terminals:
connected = True
if not (self.terminals[term].inputConnection or self.terminals[term].outputConnection):
connected = False
if not isinstance(self.terminals[term], TerminalPad):
if hide and not connected:
self.terminals[term].hide()
else:
self.terminals[term].show()
lay = self.nodeGraphic.layout
for wid in (lay.itemAt(i).widget() for i in range(1, lay.count())):
if not isinstance(wid, Terminal) and hasattr(wid, 'setVisible'):
wid.setVisible(not hide)
if not self.hidden:
if self.nodeGraphic.lastSize:
self.nodeGraphic.resize(self.nodeGraphic.lastSize)
self.manageTerminals()
self.nodeGraphic.collapse_btn.setIcon(get_icon('expand_more'))
else:
self.nodeGraphic.lastSize = self.nodeGraphic.size()
self.nodeGraphic.resizeToMinimum()
self.nodeGraphic.collapse_btn.setIcon(get_icon('expand_less'))
[docs] def manageTerminals(self):
'''
This method is called when users select options that need to hide or
show terminals based on selected options within the `Node`. This
method is meant to be overridden.
'''
pass
[docs] def flipTerminals(self):
'''
Loop over terminals and flip them to/from left and right
'''
for term in self.terminals:
self.terminals[term].flipped = None
self.nodeGraphic.updateConnections()
[docs] def showHelpDoc(self):
'''
Display the help documentation
'''
msg = QtWidgets.QMessageBox(self.parent)
msg.setWindowTitle('Help')
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
# use pygments to format help
if pyg:
lexer = get_lexer_by_name("python", stripnl=True, stripall=True)
lexer.add_filter("highlight", names=['Input', 'Terminals', 'Output'])
formatter = get_formatter_by_name('html', full=True, lineseparator="<br>")
msg.setText(pyg.highlight(self.__doc__, lexer, formatter))
else:
msg.setText(self.__doc__)
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg.exec_()
def showError(self):
msg = QtWidgets.QMessageBox(self.parent)
msg.setWindowTitle('Error')
msg.setText(self.errorMessage)
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg.exec_()
def toggleForceRun(self):
self.forceRun = not self.forceRun
self.nodeGraphic.forceRunOption.setChecked(self.forceRun)
self.nodeGraphic.force_run_btn.setChecked(self.forceRun)
def setLayer(self, value):
self.layer = value
self.nodeGraphic.setZValue(value)
def showWarning(self, message, btns=QtWidgets.QMessageBox.Ok,
defaultBtn=None):
warning = QtWidgets.QMessageBox(self.parent)
warning.setIcon(QtWidgets.QMessageBox.Warning)
warning.setText(message)
warning.setStandardButtons(btns)
if defaultBtn:
warning.setDefaultButton(defaultBtn)
return warning.exec_()
def setCustomState(self, customParams=None):
return {}
def getCustomState(self, customState, customParams=None):
pass
def deleted(self):
pass
class ResizeWidget(QtWidgets.QWidget):
sizeChangeEvent = QtCore.Signal()
def __init__(self, parent=None, nodeGraphic=None):
QtWidgets.QWidget.__init__(self, parent)
self.nodeGraphic = nodeGraphic
def resizeEvent(self, event):
self.sizeChangeEvent.emit()
QtWidgets.QWidget.resizeEvent(self, event)
def __getstate__(self):
pass
def __setstate__(self, state):
pass
[docs]class NodeGraphic(QtWidgets.QGraphicsProxyWidget):
'''
This class handles all the graphics for a Node.
Parameters
----------
node (Node):
the `Node` that this `NodeGraphic` is part of
parent (QObject):
parent (default None)
style (dict):
dictionary containing options
Signals
-------
rightClickEvent (QGraphicsEvent):
emits when a user right clicks on a node but not over a `Terminal`
'''
rightClickEvent = QtCore.Signal(object)
def __init__(self, node, parent=None, style={}):
QtWidgets.QGraphicsProxyWidget.__init__(self, parent)
self.w = ResizeWidget(nodeGraphic=self)
self.w.setMouseTracking(True)
self.setWidget(self.w)
self.move = False
self.error = False
self.mouseIsPressed = False
self.mousePressPos = QtCore.QPointF(0, 0)
self.acceptTerminals = False
self.node = node
self.name = self.node.name
self.terminalDict: Mapping[str, Any] = {}
self.terminalPadNum = 0
self.selected = False
self.onlypositivecoords = False
self.rightClickMenu = None
self.lastSize = None
self.style = {'color': 'grey',
'title-color': 'black',
'title-background': '#DCDCDC',
'title-size': 14,
'title': True,
'shadow': True,
'sizegripsize': 10}
self.style.update(style)
self.layout = QtWidgets.QVBoxLayout(self.w)
self.w.setLayout(self.layout)
# Create a resize handle
self.resizeGrip = QtWidgets.QSizeGrip(self.w)
self.resizeGrip.resize(self.style['sizegripsize'],
self.style['sizegripsize'])
self.resizeGrip.show()
# build title and node level option btns
if self.style['title']:
lb = self.label = Title(self.node.name, self.w, self)
# expert mode
eb = self.expert_mode_btn = ToolButton(self.w)
eb.setIcon(get_icon('school'))
eb.setCheckable(True)
eb.setChecked(False)
eb.setVisible(False)
eb.setToolTip('Show additional options')
# Error btn
et = self.show_error_btn = ToolButton(self.w)
et.setIcon(get_icon('warning'))
et.clicked.connect(self.node.showError)
et.setToolTip('Show error message')
et.setEnabled(False)
# Force run btn
ft = self.force_run_btn = ToolButton(self.w)
ft.clicked.connect(self.node.toggleForceRun)
ft.setIcon(get_icon('run'))
ft.setCheckable(True)
ft.setChecked(False)
ft.setVisible(True)
ft.setToolTip('Always run node')
# collapse
bt = self.collapse_btn = ToolButton(self.w)
bt.clicked.connect(lambda: self.node.collapse(None))
bt.setIcon(get_icon('expand_more'))
bt.setVisible(True)
bt.setToolTip('Expand/Collapse Node')
# create the layout, add btns
wt = self.title_widget = ProgressBar(self.w, self.style)
wt.setObjectName('title_widget')
ly = QtWidgets.QHBoxLayout(wt)
ly.setSpacing(0)
ly.setContentsMargins(5, 5, 5, 5)
for w in [lb, eb, et, ft, bt]:
ly.addWidget(w)
ly.insertStretch(1, 10)
self.layout.addWidget(wt)
else:
self.label = None
self.expert_mode_btn = None
self.title_widget = None
self.show_error_btn = None
self.force_run_btn = None
self.labelEditWidget = None
self.layout.setContentsMargins(0, 0, 0, 8)
self.layout.setSpacing(0)
# set flags
for flag in [QtWidgets.QGraphicsItem.ItemIsMovable,
QtWidgets.QGraphicsItem.ItemIsSelectable]:
self.setFlag(flag, True)
self.setAcceptDrops(True)
if self.style['shadow']:
self.shadow = QtWidgets.QGraphicsDropShadowEffect()
self.shadow.setOffset(5, 5)
self.shadow.setBlurRadius(10)
self.setGraphicsEffect(self.shadow)
else:
self.shadow = None
self.manualResize = False
# signals
self.w.sizeChangeEvent.connect(self.resizeNode)
self.__initRightClickMenu__()
def __initRightClickMenu__(self):
'''
Initiates QActions and connections for node right click menu
'''
self.rightClickBaseList: Iterable[Any] = []
if self.node.error:
self.errorMess = QtWidgets.QAction('Show Error', self)
self.errorMess.triggered.connect(self.node.showError)
self.rightClickBaseList = [self.errorMess]
self.collapse_action = QtWidgets.QAction('Expand/Collapse', self)
self.collapse_action.triggered.connect(lambda: self.node.collapse(None))
self.flipTerms = QtWidgets.QAction('Flip Terminals', self)
self.flipTerms.triggered.connect(self.node.flipTerminals)
self.helpDoc = QtWidgets.QAction('Help', self)
self.helpDoc.triggered.connect(self.node.showHelpDoc)
self.forceRunOption = QtWidgets.QAction('Force Run', self)
self.forceRunOption.setCheckable(True)
self.forceRunOption.setChecked(self.node.forceRun)
self.forceRunOption.triggered.connect(self.node.toggleForceRun)
self.rightClickBaseList += [self.collapse_action,
self.flipTerms,
self.helpDoc,
self.forceRunOption]
def update_progress(self, val):
if self.title_widget is None:
return
self.title_widget.set_progress(val)
self.update(QtCore.QRectF(self.title_widget.rect()))
def moveResizeGrip(self):
# move the resize grip to the bottom right of the widget
self.resizeGrip.move(self.w.width()-self.resizeGrip.width(),
self.w.height()-self.resizeGrip.height())
[docs] def setStyle(self, style):
self.style.update(style)
if self.shadow:
self.shadow.setEnabled(self.style['shadow'])
if self.title_widget:
self.title_widget.setVisible(self.style['title'])
self.title_widget.setStyleSheet(
'QWidget{{font-size:{}pt;color:{}}}'.format(
self.style['title-size'], self.style['title-color']))
[docs] def keyPressEvent(self, event):
self.node.keyPressEvent(event)
QtWidgets.QGraphicsProxyWidget.keyPressEvent(self, event)
[docs] def rename(self, name):
'''
Rename the node.
Parameters
----------
name (str):
string to change the node name to
'''
n = str(name)
if self.label and n:
self.node.name = n
self.label.setValue(n)
if self.labelEditWidget is not None:
self.labelEditWidget.hide()
[docs] def addTerminal(self, opts, style={}, insert=None):
'''
Add a terminal to the Node.
Parameters
----------
opts (dict):
a dictionary with options to define the terminal
style (dict):
a dictionary descripting the style of the terminal
insert (int):
the position in the layout to insert the terminal at. Index
starts at 0. Thfirst item in the layout is typical the label
for the node (default None)
Returns
-------
term (Terminal):
the `Terminal` that was just created
'''
term = Terminal(opts, self.node, style=style)
term.valueChanged.connect(self.node.terminalChanged)
if isinstance(insert, int):
self.layout.insertWidget(insert, term)
else:
self.layout.addWidget(term)
self.terminalDict[opts['name']] = term
QtWidgets.QApplication.processEvents()
term.updateTerminal()
return term
[docs] def addTerminalPad(self, pos=None, connect=True, rect=None, name=None,
inout=None, dtype=None, padtype='pass', load=False):
'''
Add an arbitrary terminal pad to the :class:`Node`
Parameters
----------
pos (QtCore.QPoint):
position where to add the pad (default None)
connect (bool):
True to emit a new connection event (default True)
rect (QtCore.QRectF):
rectangle describing the geometry of the pad (default None)
name (str):
name of the pad (default None)
inout (str):
'in' for an input pad, 'out' for an output pad.
dtype (type):
dtype of the pad
padtype (str):
type of the pad either 'pass', 'index', or 'shift' for a pass
through terminal, an indexed terminal, or a shifted terminal.
'''
if rect is None:
width = self.geometry().width()
height = self.geometry().height()
if pos.y() < 15 or pos.y() > height-15:
return
rect = None
if pos.x() < 15:
rect = QtCore.QRectF(0, pos.y()-7, 15, 15)
inout = 'in'
elif pos.x() > width-15:
rect = QtCore.QRectF(width-15, pos.y()-7, 15, 15)
inout = 'out'
if rect:
if name is None:
name = inout+'{}'.format(self.terminalPadNum)
self.terminalPadNum += 1
term = TerminalPad(self, self.node, rect, name, inout=inout,
style=self.style, dtype=dtype, padtype=padtype,
load=load)
if padtype != 'loop_iterator':
self.node.terminalPadList.append(term)
self.update(rect)
self.node._addPad(term)
if connect:
self.node.newConnectionEvent.emit(term, pos, inout)
return term
return None
[docs] def deletePad(self, pad):
'''
Delete the pad
'''
# if it is a shift pad, change padtype of shiftpad
if pad.padType == 'shift':
pad.shiftPad.padType = 'pass'
pad.shiftPad.shiftPad = None
pad.removeAllConnections()
self.node.terminalPadList.remove(pad)
self.terminalDict.pop(pad.name)
self.node.terminals.pop(pad.name)
return pad
def inTermPad(self, point):
for term in self.node.terminalPadList:
if term.rect.contains(point):
return term
return False
[docs] def resizeToMinimum(self):
'''
Resize the `NodeGraphic` to the minimum size allowed by the widgets
in the node.
'''
self.layout.invalidate()
self.layout.activate()
self.resize(self.minimumSize())
# This process events is needed to allow the items in the layout to
# decide what the minimsize actually is
QtWidgets.QApplication.instance().processEvents()
self.manualResize = False
def resizeNode(self):
self.updateTerminals()
self.updateConnections()
self.moveResizeGrip()
self.manualResize = True
[docs] def removeTerminal(self, term):
'''
Remove a terminal from the node.
Parameters
----------
term (Terminal):
the terminal object to remove from the NodeGraphic
'''
term.removeAllConnections()
self.layout.removeWidget(term)
term.deleteLater()
self.terminalDict.pop(term.name)
[docs] def moveTo(self, pos):
'''
Move Node to position.
Parameters
----------
pos (QPointF):
position to move the NodeGraphic to.
'''
self.setPos(pos)
#self.updateConnections()
[docs] def screenPos(self):
'''
calculates the upper left hand corner of a node regardless of layering
of structures
'''
# Geth the position of the node relative to the scene its in
sceneToNodeVect = self.node.nodeGraphic.scenePos()
# this gets view that the node is in
nodeView = self.node.nodeGraphic.scene().views()[0]
# Get the vector from the UL corner of the view to the UL corner of the
# node at any scale (map from Scene should account for scale)
viewToNodeVect = nodeView.mapFromScene(sceneToNodeVect)
# Check to see if the node is inside of a Structure, if so, need more
# translation, if not, ready to map to screen position
if hasattr(self.node.parent.parent(), 'nodeGraphic'):
# Initialize QPoint which represents a translation vector at a
# a scale of unity for to get from the UL corner of the outer most
# structure node to the UL corner of the inner most Structure node
totalStructureTrans = QtCore.QPoint(0, 0)
# get the Node Graphic to the structure the node sits in
structureNodeGraphic = self.node.parent.parent().nodeGraphic
nStructures = 0
# Loop over the structures from the structure the Node is in out
while True:
# count the number of structures to account for the borders
nStructures += 1
# get the scene position of the strucre node in its parent
# scene
structureScenePos = structureNodeGraphic.scenePos()
# get the view that the structure node sits in
structureGView = structureNodeGraphic.scene().views()[0]
# Get the vector from the UL corner of structure node to the UL
# corner of the view that the structure node sits in
viewToStructureVect = structureGView.mapFromScene(
structureScenePos)
# check to see if the current structure sits in another
# structure, if not break out of loop
if not hasattr(structureNodeGraphic.node.parent.parent(),
'nodeGraphic'):
break
# add the view to node vector to the total translation
totalStructureTrans += viewToStructureVect
# set the current node graphic to the next structure in the
# embedded series
structureNodeGraphic \
= structureNodeGraphic.node.parent.parent().nodeGraphic
# At the exit of this loop, we should now have the properties of
# the top level strucure and the view it sits in
# Calculate the screen position of the scien that the top level
# structure sits in
screenToTopViewVect = structureGView.mapToGlobal(
QtCore.QPoint(0, 0))
# The only scale that matters is that of the Top Level View
scale = structureGView.currentScale
# This is where the subtlety comes in. The Only view that has a
# scale different than unity is the UPPER most view, thus we need
# to scale all distances if they are on an inner view.
# thus the only vector that is scaled automatically is the
# viewToStructureVect which is the distance to the top View ULC
# to the outer most structure. Everything else needs to be scaled,
# aside from the screenToTopView vector which is never scaled
# The border thickness also needs to be accounted for which is 15
# pixels plus a single pixel black line which also needs scaling
pos = screenToTopViewVect + viewToStructureVect + \
(totalStructureTrans + QtCore.QPoint(16, 16) * nStructures +
viewToNodeVect) * scale
else:
# If there were no structures present, all that is left is to find
# the vector from the screen corner to the top viewview
screenToTopViewVect = nodeView.mapToGlobal(QtCore.QPoint(0, 0))
scale = nodeView.currentScale
# sum the vectors from screen Origin to UL view corner and the
# UL view corner to the UL Node corner
pos = screenToTopViewVect + viewToNodeVect
return pos, scale
[docs] def updateTerminals(self):
'''
Update Connections
'''
# Terminal connections
for term in self.terminalDict.values():
if isinstance(term, Terminal):
term.updateTerminal()
[docs] def updateTerminalValues(self):
'''
Update Connections
'''
# Terminal connections
for term in self.terminalDict.values():
term.updateWidget()
[docs] def updateConnections(self):
'''
Update Connections
'''
# Terminal connections
for term in self.terminalDict.values():
term.updateConnections()
# update pads
for pad in self.node.terminalPadList:
pad.updateConnections()
[docs] def hoverEnterEvent(self, event):
child = self.widget().childAt(event.pos().toPoint())
if isinstance(child, QtWidgets.QWidget):
QtWidgets.QGraphicsProxyWidget.hoverMoveEvent(self, event)
else:
QtWidgets.QGraphicsItem.hoverEnterEvent(self, event)
[docs] def hoverLeaveEvent(self, event):
child = self.widget().childAt(event.pos().toPoint())
QtWidgets.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor)
if isinstance(child, QtWidgets.QWidget):
QtWidgets.QGraphicsProxyWidget.hoverMoveEvent(self, event)
else:
QtWidgets.QGraphicsItem.hoverLeaveEvent(self, event)
[docs] def hoverMoveEvent(self, event):
child = self.widget().childAt(event.pos().toPoint())
QtWidgets.QApplication.restoreOverrideCursor()
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor)
if is_title_bar(child):
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.OpenHandCursor)
QtWidgets.QGraphicsItem.hoverMoveEvent(self, event)
# Over a pad
elif isinstance(child, Terminal) or self.inTermPad(event.pos()):
if not isinstance(child, Terminal):
child = self.inTermPad(event.pos())
if child:
inout, _ = child.pointInTerminal(event.pos())
if inout:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.CrossCursor)
QtWidgets.QGraphicsItem.hoverMoveEvent(self, event)
# Not over a terminal
elif child is None:
QtWidgets.QGraphicsItem.hoverMoveEvent(self, event)
# over graphics scene
elif isinstance(child, QtWidgets.QWidget):
QtWidgets.QGraphicsProxyWidget.hoverMoveEvent(self, event)
else:
QtWidgets.QGraphicsItem.hoverMoveEvent(self, event)
[docs] def mousePressEvent(self, event):
"""
Capture mouse press events and find where the mouse was pressed on the
object.
"""
self.mousePressPos = event.pos()
self.mouseIsPressed = True
# move the node to the top
if self.scene() is not None:
if self.node.layer < self.scene().topLayer:
self.scene().topLayer += 1
self.node.setLayer(self.scene().topLayer)
# Left Click
if event.button() == QtCore.Qt.LeftButton:
child = self.widget().childAt(self.mousePressPos.toPoint())
# emit lost focus from structure nodes to clear selected items properly
if hasattr(self.node, ' nodeWidget'):
if child is None:
self.node.nodeWidget.scene.\
focusOutEvent(QtGui.QFocusEvent(QtCore.QEvent.FocusOut))
# case node
elif hasattr(self.node, 'nodeWidgets'):
if child is None:
self.node.switch.currentWidget().scene.\
focusOutEvent(QtGui.QFocusEvent(QtCore.QEvent.FocusOut))
# Over Title
if is_title_bar(child):
self.move = True
self.setCursor(QtCore.Qt.ClosedHandCursor)
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
# Over pad
elif isinstance(child, Terminal) or self.inTermPad(event.pos()):
self.move = False
if child is None:
child = self.inTermPad(event.pos())
inout, _ = child.pointInTerminal(self.mousePressPos)
if inout:
self.node.newConnectionEvent.emit(child, self.mapToScene(self.mousePressPos), inout)
#QtWidgets.QGraphicsItem.mousePressEvent(self, event)
# Not over widget
elif child is None:
self.move = True
self.setCursor(QtCore.Qt.ClosedHandCursor)
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
# Everything else
else:
self.move = False
QtWidgets.QGraphicsProxyWidget.mousePressEvent(self, event)
# Right Click
elif event.button() == QtCore.Qt.RightButton:
child = self.widget().childAt(self.mousePressPos.toPoint())
if child is None:
self.rightClickEvent.emit(event)
else:
if is_title_bar(child):
self.showRightClickMenu(QtGui.QCursor.pos())
else:
QtWidgets.QGraphicsProxyWidget.mousePressEvent(self, event)
else:
QtWidgets.QGraphicsProxyWidget.mousePressEvent(self, event)
[docs] def mouseDoubleClickEvent(self, event):
"""
Capture double click event
"""
if event.button() == QtCore.Qt.LeftButton:
child = self.widget().childAt(self.mousePressPos.toPoint())
if is_title_bar(child):
if self.labelEditWidget is None:
self.buildLabelEditWidget()
if self.labelEditWidget.parent() is None:
self.labelEditWidget.setParent(self.scene().views()[0])
sceneGeometry = self.mapToScene(QtCore.QRectF(child.geometry()))
self.labelEditWidget.setGeometry(self.scene().views()[0].mapFromScene(sceneGeometry).boundingRect())
self.labelEditWidget.move(self.scene().views()[0].mapFromScene(self.mapToScene(child.pos())))
self.labelEditWidget.show()
self.labelEditWidget.setFocus()
QtWidgets.QGraphicsProxyWidget.mouseDoubleClickEvent(self, event)
[docs] def mouseReleaseEvent(self, event):
"""
Capture mouse Release events.
"""
self.mouseIsPressed = False
if self.move:
self.move = False
QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
#self.scene().updateSceneBounds()
else:
QtWidgets.QGraphicsProxyWidget.mouseReleaseEvent(self, event)
[docs] def mouseMoveEvent(self, event):
"""
Handle mouse move events.
"""
if self.move and self.mouseIsPressed:
self.scene().dragging = True
QtWidgets.QGraphicsItem.mouseMoveEvent(self, event)
else:
QtWidgets.QGraphicsProxyWidget.mouseMoveEvent(self, event)
# restrict nodes to positive coordinates
if self.onlypositivecoords:
if self.scenePos().x() < 0:
self.setX(0)
if self.scenePos().y() < 0:
self.setY(0)
self.updateConnections()
[docs] def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
if self.scene() is not None and self.scene().parent is not None and self.scene().parent.snapToGrid:
gridsize = self.scene().parent.gridSize
newPos = value
newPos.setX(int(newPos.x()/gridsize)*gridsize)
newPos.setY(int(newPos.y()/gridsize)*gridsize)
else:
self.updateConnections()
return QtWidgets.QGraphicsProxyWidget.itemChange(self, change, value)
[docs] def dropConnectionEvent(self, scenePos):
'''
Handle Drop Events
'''
connected = False
for term in self.terminalDict.values():
inout, _ = term.pointInTerminal(self.mapFromScene(scenePos))
if inout:
self.node.newConnectionEvent.emit(term, scenePos, inout)
connected = True
break
if not connected and self.acceptTerminals:
self.addTerminalPad(self.mapFromScene(scenePos))
[docs] def update(self, rect=None):
if rect is None:
rect = self.rect()
QtWidgets.QGraphicsProxyWidget.update(self, rect)
def bracketPath(self, rect):
path = QtGui.QPainterPath()
top = 1
bottom = 9
start = -2
stop = -4
# Left
path.moveTo(rect.left() + (rect.width() / 2)+start, rect.top()+top)
path.lineTo(rect.left() + (rect.width() / 2)+stop, rect.top()+top)
path.lineTo(rect.left() + (rect.width() / 2)+stop, rect.top()+bottom)
path.lineTo(rect.left() + (rect.width() / 2)+start, rect.top()+bottom)
# Right
path.moveTo(rect.left() + (rect.width() / 2)-start, rect.top()+top)
path.lineTo(rect.left() + (rect.width() / 2)-stop, rect.top()+top)
path.lineTo(rect.left() + (rect.width() / 2)-stop, rect.top()+bottom)
path.lineTo(rect.left() + (rect.width() / 2)-start, rect.top()+bottom)
return path
def triangle_path(self, rect, up=True):
path = QtGui.QPainterPath()
if up:
path.moveTo(rect.left() + (rect.width() / 2.0), rect.top())
path.lineTo(rect.bottomLeft())
path.lineTo(rect.bottomRight())
path.lineTo(rect.left() + (rect.width() / 2.0), rect.top())
else:
path.moveTo(rect.left() + (rect.width() / 2.0), rect.bottom())
path.lineTo(rect.topLeft())
path.lineTo(rect.topRight())
path.lineTo(rect.left() + (rect.width() / 2.0), rect.bottom())
return path
[docs] def paint(self, painter, option, widget):
QtWidgets.QGraphicsProxyWidget.paint(self, painter, option, widget)
if self.isSelected():
painter.setPen(QtCore.Qt.blue)
painter.drawRect(self.rect())
elif self.node.error:
painter.setPen(QtCore.Qt.red)
painter.drawRect(self.rect())
elif self.node.running:
painter.setPen(QtCore.Qt.green)
painter.drawRect(self.rect())
painter.setRenderHint(QtGui.QPainter.Antialiasing)
if self.node.terminalPadList:
for terminalPad in self.node.terminalPadList:
rect = terminalPad.rect
color, _ = connection_type_func(terminalPad.dtype, terminalPad.style, term=True)
pen = QtGui.QPen(color)
painter.setPen(pen)
painter.setBrush(color)
painter.drawRect(rect)
size = 10
rect = QtCore.QRectF(QtCore.QPointF(rect.center().x()-size/2.0,
rect.center().y()-size/2.0),
QtCore.QSizeF(size, size))
color, linestyle = connection_type_func(terminalPad.dtype, terminalPad.style)
pen.setColor(color)
painter.setPen(pen)
painter.setBrush(color)
if terminalPad.padType == 'index' or terminalPad.padType == 'index_array':
pen.setWidthF(1.5)
painter.setBrush(QtGui.QBrush())
painter.setPen(pen)
painter.drawPath(self.bracketPath(rect))
elif terminalPad.padType == 'shift':
painter.fillPath(self.triangle_path(rect, terminalPad.opts['out']), color)
elif terminalPad.padType == 'variable':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), terminalPad.name)
#painter.drawText(rect.x(), rect.bottom(), 'V')
elif terminalPad.padType == 'constant':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), terminalPad.name)
#painter.drawText(rect.x(), rect.bottom(), 'C')
elif terminalPad.padType == 'return':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), ' R')
elif terminalPad.padType == 'function':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), 'F')
class TerminalPad(QtCore.QObject):
'''
A terminal pad is used if arbitrary pads are accepted by the Node (Loop).
Parameters
----------
parent (QObject):
the parent (default None)
node (Node):
the Node that this terminal is part of
rect (QRectF):
the rectangle of the pad graphic
name (str):
name of the pad
inout(str):
determine if the pad is an input or an output, either 'in' or 'out',
respectively (default 'in')
style (dict):
a dictionary describing the style (default {})
Signals
-------
valueChanged (object):
emits the current value every time the value is changed
padDeleteEvent (object):
emits a delete event for the sister shift pad if it is a shift pad
'''
valueChanged = QtCore.Signal(object)
padDeleteEvent = QtCore.Signal(object)
def __init__(self, parent, node, rect, name, inout='in', style={},
dtype=None, padtype='pass', load=False):
QtCore.QObject.__init__(self)
self.opts = {'in': False,
'out': False,
'multiinput': False,
'rect': [rect.x(), rect.y(), rect.width(), rect.height()],
'termpad': True,
'name': name,
'inout': inout,
'dtype': dtype,
'padtype': padtype,
'shiftpad': None,
'flipped': False}
self.style: Mapping[str, Any] = {}
self.style.update(style)
self.rect = rect
self.mousePos = None
self.outputConnection: Iterable[Any] = []
self.inputConnection: Iterable[Any] = []
self.parentProxy = parent
self._value = None
self.node = node
self.nodeName = node.uniqueName
self.inputValue = None
self._shiftPad = None
self.widget = None
self.scrollArea = None
self.dlg = None
self.y = rect.y()
self.label = None
if inout == 'in':
self.opts['in'] = True
else:
self.opts['out'] = True
if not load:
self.setPadType(self.padType)
def setStyle(self, style):
self.style.update(style)
def __getstate__(self):
# return only what you want to pickle
return self._value, self.opts, self.padType, self.inputConnection, self.nodeName
def __setstate__(self, state):
self._value, self.opts, self.padType, self.inputConnection, self.nodeName = state
@property
def name(self):
return self.opts['name']
@name.setter
def name(self, value):
self.opts['name'] = value
@property
def padType(self):
return self.opts['padtype']
@padType.setter
def padType(self, value):
self.opts['padtype'] = value
@property
def shiftPad(self):
return self._shiftPad
@shiftPad.setter
def shiftPad(self, value):
self._shiftPad = value
if value is not None:
self.opts['shiftpad'] = value.name
else:
self.opts['shiftpad'] = None
def setPadType(self, padType):
self.padType = padType
if self.padType == 'shift':
c = self.rect.center()
if self.opts['in']:
c.setX(self.node.nodeGraphic.geometry().width()-3)
else:
c.setX(3)
self.shiftPad = self.node.nodeGraphic.addTerminalPad(c, connect=False)
self.shiftPad.padType = self.padType
self.shiftPad.dtype = self.dtype
self.shiftPad.shiftPad = self
elif self.shiftPad is not None:
self.node.nodeGraphic.deletePad(self.shiftPad)
self.padDeleteEvent.emit(self.shiftPad)
self.shiftPad = None
@property
def dtype(self):
return self.opts['dtype']
@dtype.setter
def dtype(self, value):
self.opts['dtype'] = value
def monitorValue(self, name=''):
if not self.dlg:
self.dlg = QtWidgets.QDialog()
self.dlg.setWindowFlags(QtCore.Qt.WindowTitleHint)
self.dlg.setWindowTitle(name)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.dlg.setLayout(layout)
self.scrollArea = QtWidgets.QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.dlg.layout().addWidget(self.scrollArea)
msg = QtWidgets.QLabel()
msg.setText(str(self.value))
self.scrollArea.setWidget(msg)
pal = self.scrollArea.palette()
pal.setColor(self.scrollArea.backgroundRole(), QtCore.Qt.white)
self.scrollArea.setPalette(pal)
self.dlg.show()
#def widgetValueChanged(self, value):
# self.valueChanged.emit(value)
def update(self):
self.parentProxy.update(self.rect)
@property
def value(self):
'''
The current value.
'''
return self._value
def setValue(self, value):
'''
Set the current value.
'''
self._value = value
self.dtype = type(value)
def updateWidget(self):
if self.dlg:
self.scrollArea.setWidget(QtWidgets.QLabel(str(self.value)))
def isConnected(self):
return (len(self.inputConnection) + len(self.outputConnection)) > 0
def connectionCount(self):
return (len(self.inputConnection) + len(self.outputConnection))
def dependency(self):
dep: Iterable[Any] = []
if self.inputConnection:
dep = []
for con in self.inputConnection:
if hasattr(con.startTerm, 'nodeName') and not con.feedback:
dep.append(con.startTerm.nodeName)
return dep
def updateInput(self):
'''
If there is an input connection, update the value.
'''
if not self.inputConnection:
return False
else:
if len(self.inputConnection) == 1:
value = self.inputConnection[0].startTerm.value
if self.compare(value, self.value):
self.setValue(value)
else:
value = []
for con in self.inputConnection:
value.append(con.startTerm.value)
if self.compare(value, self.value):
self.setValue(value)
return True
return False
def compare(self, value1, value2):
'''
Compare two values to see if they are the same
'''
# first check the types
if type(value1) != type(value2):
return True
elif np and isinstance(value1, np.ndarray):
return not np.all(value1 == value2)
elif value1 == value2:
return False
else:
return True
def setInput(self, connection):
self.inputConnection.append(connection)
def setOutput(self, connection):
self.outputConnection.append(connection)
def updateConnections(self):
for connection in self.inputConnection + self.outputConnection:
connection.updateLine()
def getInPoint(self):
return self.parentProxy.mapToScene(self.rect.center())
def getOutPoint(self):
return self.parentProxy.mapToScene(self.rect.center())
def removeAllConnections(self):
for l in [self.inputConnection[:], self.outputConnection[:]]:
for connection in l:
connection.removeConnections()
def removeConnection(self, removeconnection):
for l in [self.inputConnection, self.outputConnection]:
for connection in l:
if removeconnection == connection:
l.remove(connection)
# Make sure the node state has changed
self.node.stateChanged = True
def pointInTerminal(self, point):
if isinstance(point, QtCore.QPointF):
point = point.toPoint()
if self.rect.contains(point):
if self.opts['in']:
return 'in', self.rect.center()
else:
return 'out', self.rect.center()
else:
return False, self.rect.center()
[docs]class Terminal(QtWidgets.QWidget):
'''
This class draws a terminal.
Parameters
----------
opts (dict):
a dictionary of options for a terminal
node (Node):
the `Node` that this terminal is a part of
style (dict):
a dictionary of termianl options (default {})
Signals
-------
valueChanged(value)
a signal that emits the the current value every time that the value is
changed
'''
valueChanged = QtCore.Signal(object)
def __init__(self, opts, node, style={}):
QtWidgets.QWidget.__init__(self)
# default options
self.opts = {'widget': None,
'in': False,
'out': False,
'showlabel': True,
'items': [],
'min': -999999,
'max': 999999,
'dtype': None,
'value': None,
'flipped': False,
'multiinput': False,
'checkable': False,
'columndelegate': {},
'rowdelegate': {},
'columns': [],
'rows': [],
'buttons': ['btn1'],
'browsetype': 'open',
'name': 'terminal',
'selectionBehavior': 'cell',
'selectionMode': 'single'}
self.opts.update(opts)
self.name = self.opts['name']
self.node = node
self.nodeName = node.uniqueName
self.parentProxy = node.nodeGraphic
self.label = None
# style
self.style = {'endpoints-size': 10} # size of the end point
self.style.update(style)
# layout
self.layout = QtWidgets.QHBoxLayout(self)
self.layout.setContentsMargins(20, 2, 20, 2) #left, top, right, bottom
self.layout.setSpacing(5)
self.setLayout(self.layout)
# inputs / outputs
self.inRect = QtCore.QRectF(0, 0, 0, 0)
self.outRect = QtCore.QRectF(0, 0, 0, 0)
self.height = 15
self.width = 15
self.flipped = self.opts["flipped"]
self.outputConnection: Iterable[Any] = []
self.inputConnection: Iterable[Any] = []
#self.mousePos = None
#self.inputValue = None
# monitoring dialog
self.scrollArea = None
self.dlg = None
# label - create and align
if self.opts['showlabel']:
self.label = Label(self.opts['name'])
if self.opts['in']:
self.label.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.layout.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
elif self.opts['out']:
self.label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
self.layout.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
else:
self.label.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter)
self.layout.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter)
self.layout.addWidget(self.label)
# widget - instantiate the widget to the type specified
self.widget = None
self._value = self.opts['value']
if self.opts['widget'] is not None:
# lineedit
if self.opts['widget'] == 'lineedit':
self.widget = LineEdit()
if self.opts['dtype'] is None:
self.opts['dtype'] = str
# checkbox
elif self.opts['widget'] == 'checkbox':
self.widget = CheckBox()
if self.opts['dtype'] is None:
self.opts['dtype'] = bool
# combobox
elif self.opts['widget'] == 'combobox':
self.widget = ComboBox()
self.widget.addItems(self.opts['items'])
if self._value is None and self.opts['items']:
self._value = self.opts['items'][0]
if self.opts['dtype'] is None:
self.opts['dtype'] = str
# comboboxcompleter
elif self.opts['widget'] == 'comboboxcomplete':
self.widget = ComboBoxCompleter(keyList=self.opts['items'])
if self.opts['dtype'] is None:
self.opts['dtype'] = str
# spinbox
elif self.opts['widget'] == 'spinbox':
self.widget = SpinBox()
self.widget.setRange(int(self.opts['min']), int(self.opts['max']))
if self.opts['dtype'] is None:
self.opts['dtype'] = int
# doublespinbox
elif self.opts['widget'] == 'doublespinbox':
self.widget = DoubleSpinBox()
self.widget.setRange(float(self.opts['min']), float(self.opts['max']))
if self.opts['dtype'] is None:
self.opts['dtype'] = float
# display
elif self.opts['widget'] == 'display':
self.widget = Display()
# browse
elif self.opts['widget'] == 'browse':
self.widget = Browse()
if self.opts['browsetype'] == 'save':
self.widget.setSave()
elif self.opts['browsetype'] == 'directory':
self.widget.setDirectory()
else:
self.widget.setFile()
if self.opts['dtype'] is None:
self.opts['dtype'] = str
# pushbutton
elif self.opts['widget'] == 'pushbutton':
self.widget = PushButton(text=self.opts['name'])
self.widget.setCheckable(self.opts['checkable'])
if self.opts['dtype'] is None:
self.opts['dtype'] = bool
# pushbuttons
elif self.opts['widget'] == 'pushbuttons':
self.widget = PushButtons(nbtns=self.opts['buttons'])
self.widget.setCheckable(self.opts['checkable'])
self.opts['dtype'] = list
# toolbutton
elif self.opts['widget'] == 'toolbutton':
self.widget = ToolButton()
self.widget.setCheckable(self.opts['checkable'])
if self.opts['dtype'] is None:
self.opts['dtype'] = bool
# toolbuttons
elif self.opts['widget'] == 'toolbuttons':
self.widget = ToolButtons(nbtns=self.opts['buttons'])
self.widget.setCheckable(self.opts['checkable'])
self.opts['dtype'] = list
elif self.opts['widget'] == 'textedit':
self.widget = TextEdit()
if self.opts['dtype'] is None:
self.opts['dtype'] = str
# table
elif self.opts['widget'] == 'table':
if self.opts['dtype'] is None:
self.opts['dtype'] = list
self.widget = Table(dtype=self.opts['dtype'],
columns=self.opts['columns'],
rows=self.opts['rows'],
columnDelegate=self.opts['columndelegate'],
rowDelegate=self.opts['rowdelegate'],
selectionBehavior=self.opts['selectionBehavior'],
selectionMode=self.opts['selectionMode'])
# list
elif self.opts['widget'] == 'list':
self.widget = List()
if self.opts['dtype'] is None:
self.opts['dtype'] = list
else:
raise ValueError('"{}" is not a valid widget'.format(self.opts['widget']))
# if the internal value is None, set it to the widget value
if self._value is None:
if self.widget is not None:
self._value = self.widget.value
# convert the type of the internal value to match the dtype specified
if self._value is not None:
if self.opts['dtype'] is list and not isinstance(self._value, list):
self._value = [self._value]
else:
# if the conversion fails self._value = None
try:
self._value = self.convertType(self._value)
except ValueError:
self._value = None
# widget independent settings
if self.opts['widget'] is not None:
self.widget.valueChanged.connect(self.widgetValueChanged)
if self.opts['value'] is not None:
self.widget.setValue(self.opts['value'])
self.widget.setParent(self)
self.layout.addWidget(self.widget)
# update the layout
self.layout.update()
[docs] def hide(self, severConnections=False):
if severConnections:
for connection in self.inputConnection:
connection.removeConnections()
for connection in self.outputConnection:
connection.removeConnections()
self.height = 0
self.width = 0
self.outRect = QtCore.QRectF(0, 0, 0, 0)
super().hide()
[docs] def show(self):
self.height = 15
self.width = 15
super().show()
def updateTerminal(self):
rect = self.rect()
if self.opts['in']:
if self.flipped:
self.inRect = QtCore.QRectF(rect.width()-self.width,
(rect.height()-self.height)/2.0,
self.width, self.height) #x,y,width,height
else:
self.inRect = QtCore.QRectF(0,
(rect.height()-self.height)/2.0,
self.width, self.height) #x,y,width,height
if self.opts['out']:
if self.flipped:
self.outRect = QtCore.QRectF(0,
(rect.height()-self.height)/2.0,
self.width, self.height) #x,y,width,height
else:
self.outRect = QtCore.QRectF(rect.width()-self.width,
(rect.height()-self.height)/2.0,
self.width, self.height) #x,y,width,height
self.layout.update()
self.update()
def widgetValueChanged(self, value):
# convert the widget to the dtype specified
self.setValue(self.convertType(value))
self.valueChanged.emit(value)
def __getstate__(self):
return self._value, self.opts, self.name, self.inputConnection, self.nodeName
def __setstate__(self, state):
self._value, self.opts, self.name, self.inputConnection, self.nodeName = state
@property
def dtype(self):
return self.opts['dtype']
@dtype.setter
def dtype(self, value):
self.opts['dtype'] = value
@property
def value(self):
'''
The current value.
'''
# set the default return value
value = self._value
#value = self.convertType(self._value)
#if terminal is an input
if self.opts['multiinput'] is True and len(self.inputConnection) == 1:
value = [value]
return value
@value.setter
def value(self, value):
'''
This allows for Terminal.value = newValue instead of
Terminal.setValue(newValue)
'''
self.setValue(value)
def convertType(self, value):
if self.dtype is None or isinstance(value, self.dtype):
return value
else:
return self.dtype(value) # pylint: disable=not-callable
[docs] def setValue(self, value, trigger_update=False):
'''
Set the current value.
'''
if self.opts['out']:
if value is not None:
self.opts['dtype'] = type(value)
self._value = value
if trigger_update or any(l.feedback for l in self.outputConnection):
self.valueChanged.emit(value)
def monitorValue(self, name=''):
if not self.dlg:
self.dlg = QtWidgets.QDialog()
self.dlg.setWindowFlags(QtCore.Qt.WindowTitleHint)
self.dlg.setWindowTitle(name)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.dlg.setLayout(layout)
self.scrollArea = QtWidgets.QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.dlg.layout().addWidget(self.scrollArea)
msg = QtWidgets.QLabel()
msg.setText(str(self.value))
self.scrollArea.setWidget(msg)
pal = self.scrollArea.palette()
pal.setColor(self.scrollArea.backgroundRole(), QtCore.Qt.white)
self.scrollArea.setPalette(pal)
self.dlg.show()
self.dlg.show()
def updateWidget(self):
if self.widget is not None and self._value is not None:
self.widget.setValue(self._value)
if self.dlg:
self.scrollArea.setWidget(QtWidgets.QLabel(str(self._value)))
def setValueAndUpdate(self, value):
self.setValue(value)
self.updateWidget()
[docs] def setStyle(self, style):
self.style.update(style)
def isConnected(self):
return (len(self.inputConnection) + len(self.outputConnection)) > 0
def connectionCount(self):
return (len(self.inputConnection) + len(self.outputConnection))
def dependency(self):
dep: Iterable[Any] = []
if self.inputConnection:
dep = []
for con in self.inputConnection:
if hasattr(con.startTerm, 'nodeName') and not con.feedback:
dep.append(con.startTerm.nodeName)
return dep
[docs] def compare(self, value1, value2):
'''
Compare two values to see if they are the same. True means the values
are NOT equal.
'''
# first check the types
if type(value1) != type(value2):
return True
# then compare the values
else:
if np and isinstance(value1, np.ndarray):
# check shape first
if value1.shape != value2.shape:
return True
return not np.all(value1 == value2)
elif pd and isinstance(value1, (pd.core.frame.DataFrame, pd.core.series.Series)):
try:
return not np.all(value1 == value2)
except ValueError:
return True
elif isinstance(value1, list):
if len(value1) != len(value2):
return True
else:
try:
return not value1 == value2
except ValueError:
try:
return not all([all(v1 == v2) for v1, v2 in zip(value1, value2)])
except:
return True
elif isinstance(value1, dict) and isinstance(value2, dict):
return True
elif value1 == value2:
return False
else:
return True
def setInput(self, connection):
self.inputConnection.append(connection)
# disable the widget since there is a connection
if self.widget is not None:
if not isinstance(self.widget, (Table, TextEdit)):
self.widget.setEnabled(False)
elif isinstance(self.widget, TextEdit):
self.widget.setReadOnly(True)
def setOutput(self, connection):
self.outputConnection.append(connection)
def getInPoint(self):
return self.parentProxy.mapToScene(self.mapToParent(self.inRect.center().toPoint()))
def getOutPoint(self):
return self.parentProxy.mapToScene(self.mapToParent(self.outRect.center().toPoint()))
def updateConnections(self):
for connection in self.inputConnection + self.outputConnection:
connection.updateLine()
[docs] def removeAllConnections(self):
'''
Remove all connections. Called at node delete event.
'''
for l in [self.inputConnection[:], self.outputConnection[:]]:
for connection in l:
connection.removeConnections()
self.inputConnection = []
self.outputConnection = []
[docs] def removeConnection(self, removeconnection):
'''
Remove a specific connection. The connection should already be removed
from the scene.
Parameters
----------
removeconnection (Connection):
the connection to be removed.
'''
for l in [self.inputConnection, self.outputConnection]:
for connection in l:
if removeconnection == connection:
l.remove(connection)
# Check to see if we need to enable the widget
if self.widget is not None and not self.inputConnection:
self.widget.setEnabled(True)
if isinstance(self.widget, TextEdit):
self.widget.setReadOnly(False)
# Make sure the node state has changed
self.node.stateChanged = True
def pointInTerminal(self, point):
if isinstance(point, QtCore.QPointF):
point = point.toPoint()
point = self.mapFromParent(point)
if (self.inRect is not None and self.inRect.contains(point)):
return 'in', self.mapToParent(self.inRect.center().toPoint())
elif (self.outRect is not None and self.outRect.contains(point)):
return 'out', self.mapToParent(self.outRect.center().toPoint())
else:
return False, None
@property
def flipped(self):
return self.opts['flipped']
@flipped.setter
def flipped(self, value):
# flip the inputs and outputs
if value is None:
self.opts['flipped'] = not self.opts['flipped']
else:
self.opts['flipped'] = value
self.setLabelLayout(self.opts['flipped'])
self.updateTerminal()
def setLabelLayout(self, flipped):
if self.opts['in']:
if self.opts['flipped']:
self.layout.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
else:
self.layout.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
elif self.opts['out']:
if self.opts['flipped']:
self.layout.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
else:
self.layout.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
[docs] def paintEvent(self, event):
QtWidgets.QWidget.paintEvent(self, event)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
color = connection_type_func(self.dtype, self.style, term=True)[0]
painter.setPen(color)
painter.setBrush(color)
if self.opts['in']:
painter.drawRect(self.inRect)
if self.opts['out']:
painter.drawRect(self.outRect)
# draw triangle to denote input or output
color = connection_type_func(self.dtype, self.style)[0]
rect = QtCore.QRectF(0, 0,
self.style['endpoints-size'],
self.style['endpoints-size'])
if self.opts['in']:
rect.moveCenter(self.inRect.center())
if self.opts['flipped']:
painter.fillPath(triangle_path(rect, 'right'), color)
else:
painter.fillPath(triangle_path(rect, 'left'), color)
elif self.opts['out']:
rect.moveCenter(self.outRect.center())
if self.opts['flipped']:
painter.fillPath(triangle_path(rect, 'right'), color)
else:
painter.fillPath(triangle_path(rect, 'left'), color)
[docs]class Connection(QtWidgets.QGraphicsItem):
'''
This class draws connections between Terminals on Nodes.
'''
def __init__(self, nodeWidget=None, style=None, parent=None):
QtWidgets.QGraphicsItem.__init__(self, parent)
self.path = None
self.startPoint = None
self.endPoint = None
self.shapePath = None
self.startTerm: Optional[Any] = None
self.stopTerm: Optional[Any] = None
self.dtype = None
self.pathPoints: Iterable[Any] = []
self.name = 'connection'
self.uniqueName = 'connection'
self.nodeWidget = nodeWidget
self.feedback = False
self.setZValue(100000)
# Style
self.style = {'line': 'cubic', # either 'cubic', 'line'
'line-offset': 50, # any integer
'line-color': 'type', #Qt color or 'type'
'line-selected': QtCore.Qt.darkGray,
'line-width': 2, # Thickness of the line
'line-selected-width': 5, # Thickness of the selection
'endpoints-color': 'type',
'endpoints-shape': 'square', # either 'square', 'circle'
'endpoints-size': 10, # size of the end point
'ctrlpoints-color': 'type',
'ctrlpoints-shape': 'flow', #square, circle, or flow
'ctrlpoints-size': 8,
'ctrlpoints-line-width': 2}
# Update default style with user style
if style is not None:
self.style.update(style)
for flag in [QtWidgets.QGraphicsItem.ItemIsSelectable]:
self.setFlag(flag, True)
# shadow = QtWidgets.QGraphicsDropShadowEffect()
# shadow.setOffset(5, 5)
# shadow.setBlurRadius(10)
# self.setGraphicsEffect(shadow)
self.__initRightClickMenu__()
def __initRightClickMenu__(self):
self.rightClickMenu = QtWidgets.QMenu()
# monitor tool
self.monitor = QtWidgets.QAction(self.rightClickMenu)
self.monitor.setText('Monitor')
self.monitor.triggered.connect(self.monitorConnection)
self.rightClickMenu.addAction(self.monitor)
# mark feedback connection
self.setfeeback_action = QtWidgets.QAction("Feedback connection", self.rightClickMenu)
self.setfeeback_action.setCheckable(True)
self.setfeeback_action.setChecked(False)
self.setfeeback_action.toggled.connect(self.set_feedback)
self.rightClickMenu.addAction(self.setfeeback_action)
def set_feedback(self, val):
self.feedback = val
self.updateLine()
def __getstate__(self):
return self.startTerm
def __setstate__(self, state):
self.startTerm = state
[docs] def mousePressEvent(self, event):
'''
Capture the mouse press event
Parameters
----------
event ():
mouse event
'''
pos = event.pos()
if event.button() == QtCore.Qt.LeftButton:
if self.startRect.contains(pos):
event.ignore()
elif self.stopRect.contains(pos) and self.stopTerm.opts['multiinput']:
event.ignore()
else:
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
elif event.button() == QtCore.Qt.RightButton:
if self.startRect.contains(pos) or self.stopRect.contains(pos):
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
else:
self.setSelected(True)
self.rightClickMenu.popup(QtGui.QCursor.pos())
[docs] def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.addControlPoint(self.mapToScene(event.pos()))
self.updateLine()
def monitorConnection(self):
self.startTerm.monitorValue(self.uniqueName)
def addControlPoint(self, point, flipped=False):
controlPoint = ControlPoint(parent=self, pos=point, flipped=flipped)
# find insertion point
points = [pt.center() for pt in self.pathPoints]
points.insert(0, self.startPoint)
points.append(self.endPoint)
for index, pt in enumerate(points[:-1]):
rect = QtCore.QRectF(pt,
points[index+1])
rect = rect.normalized()
if rect.contains(point):
break
# insert
self.pathPoints.insert(index, controlPoint)
[docs] def setStartTerminal(self, startTerm):
'''
Set the starting terminal for a connection.
'''
self.removeStartTerm()
self.startTerm = startTerm
self.startTerm.setOutput(self)
self.dtype = self.startTerm.dtype
self.startPoint = self.startTerm.getOutPoint()
[docs] def setStopTerminal(self, stopTerm):
'''
Set the starting terminal for a connection.
'''
self.removeStopTerm()
self.stopTerm = stopTerm
self.stopTerm.setInput(self)
self.dtype = self.stopTerm.dtype
self.endPoint = self.stopTerm.getInPoint()
[docs] def removeConnections(self):
'''
Remove self from connected Nodes.
'''
self.removeStartTerm()
self.removeStopTerm()
if self.scene(): # required for tests
self.scene().removeItem(self)
if self.nodeWidget: # required for tests
self.nodeWidget.nodeDict.pop(self.uniqueName)
[docs] def removeStartTerm(self):
'''
Remove the starting terminal
'''
if self.startTerm is not None:
# remove the monitor if one exists
if self.startTerm.dlg:
self.startTerm.dlg.done(0)
self.startTerm.dlg = None
self.startTerm.removeConnection(self)
self.startTerm = None
[docs] def removeStopTerm(self):
'''
Remove the stop terminal
'''
if self.stopTerm is not None:
self.stopTerm.removeConnection(self)
self.stopTerm = None
[docs] def isConnected(self):
'''
Check if the connection has both a start and stop terminal. It it does,
return True, else return False.
'''
#multi input test
multiinput = True
if self.stopTerm:
if self.stopTerm.opts['multiinput']:
multiinput = True
else:
multiinput = len(self.stopTerm.inputConnection) == 1
return self.startTerm is not None and \
self.stopTerm is not None and \
self.startTerm.node != self.stopTerm.node and \
multiinput
def setError(self):
pass
def clearError(self):
pass
[docs] def updateLine(self, mousePos=None):
'''
Update the line. If mousePos, use as missing point (start, stop).
Parameters
----------
mousePos (QtCore.QPoint):
a position to use as the missing point if
'''
if self.startTerm is None:
self.startPoint = mousePos
else:
self.dtype = self.startTerm.dtype
self.startPoint = self.startTerm.getOutPoint()
if self.stopTerm is None:
self.endPoint = mousePos
else:
self.endPoint = self.stopTerm.getInPoint()
self.prepareGeometryChange()
if self.startPoint is not None and self.endPoint is not None:
self.generatePath(self.pathPoints)
self.shapePath = None
self.update()
[docs] def generatePath(self, points):
'''
Generate a path following points
Parameters
----------
points (list):
a list of QtCore.QPoint points for the connection line to follow
'''
# Generate line
self.path = QtGui.QPainterPath()
xlen = self.style['line-offset']
# insert the start and end points
points.insert(0, self.startPoint)
points.append(self.endPoint)
for index, point in enumerate(points[:-1]):
point1 = point
#if start point
if index == 0:
startx = point1.x()+xlen
if self.startTerm is not None and self.startTerm.opts['flipped']:
startx = point1.x()-xlen
else:
if isinstance(point1, ControlPoint):
if point1.flipped:
startx = point1.x()-xlen
else:
startx = point1.x()+xlen
point1 = point.center()
# start at point1
self.path.moveTo(point1)
# if end point
point2 = points[index+1]
if index == len(points)-2:
stopx = point2.x()-xlen
if self.stopTerm is not None and self.stopTerm.opts['flipped']:
stopx = point2.x()+xlen
else:
if isinstance(point2, ControlPoint):
if point2.flipped:
stopx = point2.x() + xlen
else:
stopx = point2.x()-xlen
point2 = points[index+1].center()
else:
stopx = point2.x()-xlen
if self.style['line'] == 'cubic':
self.path.cubicTo(QtCore.QPoint(int(startx), int(point1.y())),
QtCore.QPoint(int(stopx), int(point2.y())),
QtCore.QPoint(int(point2.x()), int(point2.y())))
elif self.style['line'] == 'line':
# This will draw straight lines
self.path.lineTo(point2.x(), point2.y())
# Generate end points
size = self.style['endpoints-size']
self.stopRect = QtCore.QRectF(QtCore.QPointF(points[-1].x()-size/2.0,
points[-1].y()-size/2.0),
QtCore.QSizeF(size, size))
self.startRect = QtCore.QRectF(QtCore.QPointF(points[0].x()-size/2.0,
points[0].y()-size/2.0),
QtCore.QSizeF(size, size))
# remove the start and end points
points.pop(0)
points.pop(-1)
return self.path
[docs] def boundingRect(self):
'''
Return the bounding rectangle of the connection.
'''
return self.shape().boundingRect()
[docs] def shape(self):
'''
Calculate and return the shape of the connection. This shape is used
for determining if the mouse is "over" the connection. This is used for
mouseEvents and HoverEvents.
'''
if self.shapePath is None:
if self.path is None:
return QtGui.QPainterPath()
stroker = QtGui.QPainterPathStroker()
# add a buffer, this makes it easier to select with the mouse
stroker.setWidth(15)
self.shapePath = stroker.createStroke(self.path)
return self.shapePath
[docs] def state(self, customParams=None):
'''
The current state of the node (position, values, connections, path)
Returns
-------
stateDict (dict):
a JSON encodable dictionary
'''
stateDict = {'type': 'Connection',
'name': self.name,
'line':self.style['line'],
'uniquename': self.uniqueName,
'input': [self.startTerm.node.uniqueName, self.startTerm.opts['name']],
'output': [self.stopTerm.node.uniqueName, self.stopTerm.opts['name']],
'controlpoints': [],
'feedback': self.feedback,
}
for point in self.pathPoints:
stateDict['controlpoints'].append([point.center().x(),
point.center().y(), point.flipped])
return stateDict
[docs] def paint(self, painter, *args):
'''
Paint the line.
'''
# update the dtype
if self.startTerm:
self.dtype = self.startTerm.dtype
pen = QtGui.QPen()
pen.setStyle(connection_type_func(self.dtype, self.style)[1])
pen.setStyle(QtCore.Qt.DashLine if self.feedback else QtCore.Qt.SolidLine)
# if selected
if self.isSelected():
pen.setColor(self.style['line-selected'])
pen.setWidth(self.style['line-selected-width'])
painter.setPen(pen)
painter.drawPath(self.path)
# Line
if self.style['line-color'] != 'type':
color = self.style['line-color']
else:
color = connection_type_func(self.dtype, self.style)[0]
pen.setColor(color)
pen.setWidth(self.style['line-width'])
painter.setPen(pen)
painter.drawPath(self.path)
# End points
if self.style['endpoints-color'] != 'type':
color = self.style['endpoints-color']
else:
color = connection_type_func(self.dtype, self.style)[0]
pen.setColor(color)
pen.setWidth(1)
painter.setPen(pen)
painter.setBrush(color)
for rect in [self.startRect, self.stopRect]:
if self.style['endpoints-shape'] == 'circle':
painter.drawEllipse(rect)
else:
painter.drawRect(rect)
for term, rect in [(self.startTerm, self.startRect),
(self.stopTerm, self.stopRect)]:
if term and isinstance(term, TerminalPad):
color, _ = connection_type_func(self.dtype, self.style, term=True)
pen = QtGui.QPen(color)
painter.setPen(pen)
painter.setBrush(color)
if term.padType == 'index' or term.padType == 'index_array':
pen.setWidthF(1.5)
painter.setPen(pen)
painter.setBrush(QtGui.QBrush())
painter.drawPath(term.parentProxy.bracketPath(rect))
elif term.padType == 'shift':
if term.opts['out']:
painter.fillPath(triangle_path(rect, 'up'), color)
else:
painter.fillPath(triangle_path(rect, 'down'), color)
elif term.padType == 'variable':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), term.name)
#painter.drawText(rect.x(), rect.bottom(), 'V')
elif term.padType == 'constant':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), term.name)
#painter.drawText(rect.x(), rect.bottom(), 'C')
elif term.padType == 'return':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), 'R')
elif term.padType == 'function':
painter.setBrush(QtGui.QBrush())
painter.drawText(rect.x(), rect.bottom(), 'F')
elif term:
color = connection_type_func(self.dtype, self.style, term=True)[0]
painter.setPen(pen)
painter.setBrush(color)
if term.opts['in']:
if term.opts['flipped']:
painter.fillPath(triangle_path(rect, 'Right'), color)
else:
painter.fillPath(triangle_path(rect, 'Left'), color)
elif term.opts['out']:
if term.opts['flipped']:
painter.fillPath(triangle_path(rect, 'Right'), color)
else:
painter.fillPath(triangle_path(rect, 'Left'), color)
class ControlPoint(QtWidgets.QGraphicsRectItem):
'''
This class draws ControlPoints on Connections.
'''
def __init__(self, parent=None, pos=None, flipped=False):
QtWidgets.QGraphicsRectItem.__init__(self, parent)
self.parent = parent
self.style = self.parent.style
self.dtype = self.parent.dtype
self.flipped = flipped
self.setZValue(100000)
# create internal rect
self._rect = QtCore.QRectF(0, 0,
self.style['ctrlpoints-size'],
self.style['ctrlpoints-size'])
# set the current rectangle
self.setRect(self._rect)
# move to the correct position
rad = self.style['ctrlpoints-size']/2.0
self.setPos(pos-QtCore.QPointF(rad, rad))
# move the internal rectangle
self._rect.moveCenter(pos)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
def mouseMoveEvent(self, event):
"""
Handle mouse move events.
"""
self._rect.moveTo(self.pos())
QtWidgets.QGraphicsRectItem.mouseMoveEvent(self, event)
self.parent.updateLine()
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
if self.scene() is not None and self.scene().parent is not None and self.scene().parent.snapToGrid:
gridsize = self.scene().parent.gridSize
newPos = value.toPointF()
newPos.setX(int(newPos.x()/gridsize)*gridsize)
newPos.setY(int(newPos.y()/gridsize)*gridsize)
return newPos
else:
return QtWidgets.QGraphicsRectItem.itemChange(self, change, value)
else:
return QtWidgets.QGraphicsRectItem.itemChange(self, change, value)
def mousePressEvent(self, event):
"""
Capture mouse press events and find where the mouse was pressed on the
object.
"""
if event.button() == QtCore.Qt.RightButton:
self.flipped = not self.flipped
self.parent.updateLine()
QtWidgets.QGraphicsRectItem.mousePressEvent(self, event)
def mouseDoubleClickEvent(self, event):
"""
Capture double click event
"""
if event.button() == QtCore.Qt.LeftButton:
self.parent.pathPoints.remove(self)
self.scene().removeItem(self)
self.parent.updateLine()
QtWidgets.QGraphicsRectItem.mouseDoubleClickEvent(self, event)
def center(self):
return self._rect.center()
def paint(self, painter, *args):
# update the dtype
self.dtype = self.parent.dtype
self._rect.moveTo(self.pos())
pen = QtGui.QPen()
pen.setStyle(connection_type_func(self.dtype, self.style)[1])
# marker
pen.setWidth(self.style['ctrlpoints-line-width'])
if self.style['line-color'] != 'type':
color = self.style['line-color']
else:
color = connection_type_func(self.dtype, self.style)[0]
pen.setColor(color)
# if selected
if self.isSelected():
pen.setColor(self.style['line-selected'])
painter.setPen(pen)
painter.setBrush(color)
if self.style['ctrlpoints-shape'] == 'square':
painter.drawRect(self.boundingRect())
elif self.style['ctrlpoints-shape'] == 'flow':
if self.flipped:
painter.drawPath(triangle_path(self.boundingRect(), 'right'))
else:
painter.drawPath(triangle_path(self.boundingRect(), 'left'))
else:
painter.drawEllipse(self.boundingRect())