Source code for nodeworks.node

# -*- 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 addWidget(self, widget, insert=None): ''' Add an arbitrary widget to the Node. Parameters ---------- widget (QWidget): the widget to be added to the node. Can be any :class:`QtWidgets.QWidget` based class such as :class:`QtWidgets.QLineEdit`, :class:`QtWidgets.QPushButton`, etc. insert (int): the position to insert the wiget at. ''' self.nodeGraphic.addWidget(widget, insert)
[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 updateInputs(self): ''' Loop through terminals, updating inputs ''' # Loop through termianls for term in self.terminals.values(): needsrun = term.updateInput() if needsrun: self.stateChanged = True # Loop through pads for pad in self.terminalPadList: needsrun = pad.updateInput() if needsrun: self.stateChanged = True
[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 showRightClickMenu(self, globalpos, otheractions=None): ''' Creates, configures, and sets right click menu. Parameters ---------- globalpos (QPoint): the `QPoint` object that represents the position of the mouse when the function is called otheractions (list): a list of additional `QAction` items that should be added to the right click menu for the node ''' self.rightClickMenu = QtWidgets.QMenu() # is there a better solution for this? the check state for force run is not updated when # loading a flow sheet. The state of the node is loaded after the menu is created. self.__initRightClickMenu__() self.rightClickMenu.addActions(self.rightClickBaseList) if otheractions is not None: self.rightClickMenu.addActions(otheractions) self.rightClickMenu.popup(globalpos)
[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 buildLabelEditWidget(self): ''' Create a QLineEdit to edit the Nodes name. ''' lw = self.labelEditWidget = LineEdit() lw.setText(self.name) lw.lostFocus.connect(self.rename)
[docs] def setLabelEditWidget(self, widget): ''' Set a QWidget to act as an editor for renaming the node. Parameters ---------- widget (QWidget): A QWidget to be called when the user edits the node name. This widget must have a `lostFocus` signal that is emitted when the widget as a new name to set the `Node` name to. ''' self.labelEditWidget = widget self.labelEditWidget.lostFocus.connect(self.rename)
[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 addWidget(self, widget, insert=None): ''' Allows arbitrary widgets to be added to a Node. Parameters ---------- widget (QWidget): the widget to be added 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) ''' if isinstance(insert, int): self.layout.insertWidget(insert, widget) else: self.layout.addWidget(widget)
[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 updateInput(self): ''' If there is an input connection, update the value. ''' if not self.inputConnection: return False else: if len(self.inputConnection) == 1: if not hasattr(self.inputConnection[0].startTerm, 'value'): return False value = self.inputConnection[0].startTerm.value if self.compare(value, self.value): self.setValue(value) return True else: value = [] for x in self.inputConnection: if hasattr(x.startTerm, 'value'): value.append(x.startTerm.value) if self.compare(value, self.value): self.setValue(value) return True return False
[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())