Source code for gpi.node

#    Copyright (C) 2014  Dignity Health
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Lesser General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#    NO CLINICAL USE.  THE SOFTWARE IS NOT INTENDED FOR COMMERCIAL PURPOSES
#    AND SHOULD BE USED ONLY FOR NON-COMMERCIAL RESEARCH PURPOSES.  THE
#    SOFTWARE MAY NOT IN ANY EVENT BE USED FOR ANY CLINICAL OR DIAGNOSTIC
#    PURPOSES.  YOU ACKNOWLEDGE AND AGREE THAT THE SOFTWARE IS NOT INTENDED FOR
#    USE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITY, INCLUDING BUT NOT
#    LIMITED TO LIFE SUPPORT OR EMERGENCY MEDICAL OPERATIONS OR USES.  LICENSOR
#    MAKES NO WARRANTY AND HAS NO LIABILITY ARISING FROM ANY USE OF THE
#    SOFTWARE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITIES.
#
#    The code in this file was modifed/derived from the elasticnodes.py
#    example with the license:
#############################################################################
##
## Copyright (C) 2010 Riverbank Computing Limited.
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
## All rights reserved.
##
## This file is part of the examples of PyQt.
##
## $QT_BEGIN_LICENSE:BSD$
## You may use this file under the terms of the BSD license as follows:
##
## "Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are
## met:
##   * Redistributions of source code must retain the above copyright
##     notice, this list of conditions and the following disclaimer.
##   * Redistributions in binary form must reproduce the above copyright
##     notice, this list of conditions and the following disclaimer in
##     the documentation and/or other materials provided with the
##     distribution.
##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
##     the names of its contributors may be used to endorse or promote
##     products derived from this software without specific prior written
##     permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
## $QT_END_LICENSE$
##
#############################################################################

import os
import sys
import copy
import math
import inspect
import traceback
import subprocess
import numpy as np


# gpi
import gpi
from gpi import QtCore, QtGui
from .defaultTypes import GPIDefaultType
from .defines import NodeTYPE, GPI_APPLOOP, REQUIRED, GPI_SHDM_PATH
from .defines import GPI_WIDGET_EVENT, GPI_PORT_EVENT, GPI_INIT_EVENT, GPI_REQUEUE_EVENT
from .defines import printMouseEvent, getKeyboardModifiers, stw, Cl
from .defines import GetHumanReadable_bytes, GetHumanReadable_time
from .logger import manager
from .port import InPort, OutPort
from .stateMachine import GPI_FSM, GPIState
from .functor import GPIFunctor, Return
from .sysspecs import Specs

# start logger for this module
log = manager.getLogger(__name__)

# Timer Pack
[docs]class TimerPack(object): ''' GUI updates often need to be prodded at some interval for a duration, and then shutdown. This seems to require 2 timers, one for interval, one for ON duration. ''' def __init__(self): self._interval = QtCore.QTimer() self._ON = QtCore.QTimer() self._ON.setSingleShot(True) self._ON.timeout.connect(self._interval.stop) def setTimeoutSlot_interval(self, f): self._interval.timeout.connect(f) def setTimeoutSlot_ON(self, f): self._ON.timeout.connect(f) def setInterval(self, t): self._interval.setInterval(t) def setDuration(self, t): self._ON.setInterval(t) def isActive(self): return self._interval.isActive() def start(self): self._interval.start() self._ON.start()
# Event Manager
[docs]class EventManager(object): '''Stores new events (without duplicates) in a timely manner.''' def __init__(self): self._wdg_events = set() # holds wdg names self._port_events = set() # port names self._init_event = False # bool self._requeue_event = False # bool def addWidgetEvent(self, e): self._wdg_events.add(e) def addPortEvent(self, e): self._port_events.add(e) def setInitEvent(self): self._init_event = True def setRequeueEvent(self): self._requeue_event = True @property def widget(self): return self._wdg_events @property def port(self): return self._port_events @property def init(self): return self._init_event @property def requeue(self): return self._requeue_event @property def events(self): o = {} o[GPI_WIDGET_EVENT] = self._wdg_events o[GPI_PORT_EVENT] = self._port_events o[GPI_INIT_EVENT] = self._init_event o[GPI_REQUEUE_EVENT] = self._requeue_event return o def __str__(self): msg = 'widgets: '+str(self._wdg_events)+'\n' msg += 'ports: '+str(self._port_events)+'\n' msg += 'init: '+str(self._init_event)+'\n' msg += 'requeue: '+str(self._requeue_event) return msg
class NodeEvent(object): def __init__(self): self._status = None
[docs]class NodeSignalMediator(QtCore.QObject): ''' A hack to add PyQt signals to a non QObject derivative. http://kedeligdata.blogspot.com/2010/01/pyqt-emitting-events-from-non-qobject.html ''' _switchSig = gpi.Signal(str) _forceUpdate = gpi.Signal() _curState = gpi.Signal(str) def __init__(self): super(NodeSignalMediator, self).__init__()
[docs]class NodeAppearance(object): ''' This class may be used to define node color, height, width, margins,etc.. ''' def __init__(self): self._MAX_ITER = 64 self._title_font_ht = 14 self._label_font_ht = 10 self._text_font_ht = 8 self._progress_font_ht = 8 self._title_font_family = 'times' self._label_font_family = 'times' self._text_font_family = 'times' self._progress_font_family = 'times' self._title_font_pt = self.fitPointSize(self._title_font_family,self._title_font_ht) self._label_font_pt = self.fitPointSize(self._label_font_family,self._label_font_ht) self._text_font_pt = self.fitPointSize(self._text_font_family,self._text_font_ht) self._progress_font_pt = self.fitPointSize(self._progress_font_family,self._progress_font_ht) self._title_qfont = QtGui.QFont(self._title_font_family, self._title_font_pt) self._label_qfont = QtGui.QFont(self._label_font_family, self._label_font_pt) self._text_qfont = QtGui.QFont(self._text_font_family, self._text_font_pt) self._progress_qfont = QtGui.QFont(self._progress_font_family, self._progress_font_pt) def __str__(self): msg = 'title font: ' + str(self._title_qfont.family()) +', ' + str(self._title_qfont.pointSize()) +', ' + str(self._title_qfont.pixelSize())+ '\n' msg +='label font: ' + str(self._label_qfont.family()) +', ' + str(self._label_qfont.pointSize()) +', ' + str(self._label_qfont.pixelSize())+ '\n' msg +='text font: ' + str(self._text_qfont.family()) +', ' + str(self._text_qfont.pointSize()) +', ' + str(self._text_qfont.pixelSize())+ '\n' msg +='progress font: ' + str(self._progress_qfont.family()) +', ' + str(self._progress_qfont.pointSize()) +', ' + str(self._progress_qfont.pixelSize())+ '\n' return msg def titleQFont(self): return self._title_qfont def labelQFont(self): return self._label_qfont def textQFont(self): return self._text_qfont def progressQFont(self): return self._progress_qfont def fitPointSize(self, fontfamily, height): # find the point-size given height in pixels # this has to be done b/c setPixelSize() doesn't seem to work across # platforms. for pt in range(1,self._MAX_ITER): if QtGui.QFontMetricsF(QtGui.QFont(fontfamily,pt)).height() > height: return pt-1
[docs]class Node(QtGui.QGraphicsItem): '''The graphics and execution manager for individual nodes. ''' Type = NodeTYPE # These signals are being patched in so that the inner workings of the node # do not need to be abstracted from the QGraphicsItem() since it is not a # QObject(). This is just a stepping stone for migrating to a proper # solution. def _switchSig(): def fget(self): return self._mediator._switchSig return locals() _switchSig = property(**_switchSig()) def _forceUpdate(): def fget(self): return self._mediator._forceUpdate return locals() _forceUpdate = property(**_forceUpdate()) def _curState(): def fget(self): return self._mediator._curState return locals() _curState = property(**_curState()) def __init__(self, CanvasBackend, nodeCatItem=None, nodeIF=None, nodeIFscroll=None, nodeMenuClass=None): super(Node, self).__init__() self._mediator = NodeSignalMediator() # PAINTER self._drop_shadow = QtGui.QGraphicsDropShadowEffect() self._drop_shadow.setOffset(5.0,5.0) self._drop_shadow.setBlurRadius(5.0) self.setGraphicsEffect(self._drop_shadow) # keep a ref for info self.item = nodeCatItem # During re-instantiation (from .net file) this is # the old id, if the node is new then its the current id. self._id = None self.setID() self._ext_filename = None self.nodeCompute_thread = None self._computeDuration = [] self.initStateMachine() self.graph = CanvasBackend self.inportList = [] self.outportList = [] self.newPos = QtCore.QPointF() self.setFlag(QtGui.QGraphicsItem.ItemIsMovable) self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable) self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges) self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) self.setZValue(2) # event status self._event_type = None # {type:title} # deprecated self._events = EventManager() # event obj to replace _event_type self._events_handoff = None self._event_pending = False self._node_disabled = False self._requeue = False self._markedForDeletion = False self._returnCode = None # needed for passing between slots # node name self.NodeLook = NodeAppearance() self.name = "Node" self._hierarchal_level = -1 self.title_font = self.NodeLook.titleQFont() self._label_font = self.NodeLook.labelQFont() self._label_inset = 0.0 self._label_maxLen = 64 # chars self._detailLabel_font = self.NodeLook.textQFont() self._detailLabel_inset = 0.0 self.progress_font = self.NodeLook.progressQFont() # node text layout self._top_margin = 6.0 self._bottom_margin = 7.0 self._left_margin = 5.0 self._right_margin = 11.0 self._progress_done = TimerPack() self._progress_done.setTimeoutSlot_interval(self.update) self._progress_done.setTimeoutSlot_ON(self.prepareGeometryChange) self._progress_done.setTimeoutSlot_ON(self.update) self._progress_done.setInterval(100) # msec self._progress_done.setDuration(300) # done duration (when 100% is shown) self._progress_was_on = False self._progress_recalculate = 0 self._progress_recalculate2 = 0 self._progress_recalculate3 = 0 self._prog_thresh = 1 # sec # right side display width self._extra_right = 0 # progress timer self._progress_timer = QtCore.QTimer() self._progress_timer.timeout.connect(self.update) self._progress_timer.setInterval(100) # msec, redraw # RELOAD # reload linger and fade-out timers self._reload_timer = QtCore.QTimer() self._reload_timer.setSingleShot(True) self._reload_timer.timeout.connect(self.reloadDone) self._reload_timer.setInterval(2000) # msec, linger # get a menu instance if nodeCatItem: self._moduleName = nodeCatItem.name self._nodeIF = None # must exist so that it can be # recursively checked by nodeMenuClass self._nodeIF = nodeCatItem.description()(self) self._nodeIF.modifyWdg.connect(self.modifyWdg) # get module description filename self._ext_filename = nodeCatItem.editable_path # make widget menus scrollable self._nodeIF_scrollArea = QtGui.QScrollArea() self._nodeIF_scrollArea.setWidget(self._nodeIF) self._nodeIF_scrollArea.setWidgetResizable(True) self._scroll_grip = QtGui.QSizeGrip(self._nodeIF) self._nodeIF_scrollArea.setCornerWidget(self._scroll_grip) self._nodeIF_scrollArea.setGeometry(50, 50, 1000, 2000) # old-style constructor (deprecate) elif nodeMenuClass: self._moduleName = nodeMenuClass.__module__.split('_GPI')[0] if self._moduleName == '__main__': self._moduleName = "Node" self._nodeIF = None # must exist so that it can be # recursively checked by nodeMenuClass self._nodeIF = nodeMenuClass(self) self._nodeIF.modifyWdg.connect(self.modifyWdg) # get module description filename self._ext_filename = inspect.getfile(nodeMenuClass) self._ext_filename = os.path.splitext(self._ext_filename)[0] + '.py' # make widget menus scrollable self._nodeIF_scrollArea = QtGui.QScrollArea() self._nodeIF_scrollArea.setWidget(self._nodeIF) self._nodeIF_scrollArea.setWidgetResizable(True) self._nodeIF_scrollArea.setGeometry(50, 50, 1000, 2000) else: # assume nodeIF and nodeIFscroll self._nodeIF = nodeIF self._nodeIF_scrollArea = nodeIFscroll self._menuHasRaised = False if hasattr(self._nodeIF, 'updateTitle'): self._nodeIF.updateTitle() self._curState.connect(self._nodeIF.setStatus_sys) self.setAcceptHoverEvents(True) self.beingHovered = False self.updateToolTips() # make sure the node is fully loaded before starting self._machine.start(self._idleState) # process initUI return codes try: if hasattr(self._nodeIF, 'initUI_return'): if Return.isError(self._nodeIF.initUI_return()): log.error(Cl.FAIL+str(self.getName())+Cl.ESC+": initUI() failed.") self._machine.next('i_error') except: log.warn('initUI() retcode handling skipped. '+str(self.item.fullpath)) # in case node text is set in initUI self.updateOutportPosition() def loadNodeIFSettings(self, s): self._nodeIF.loadSettings(s) def getNodeDefinitionPath(self): return self._ext_filename def setDeleteFlag(self, val): self._markedForDeletion = val def isMarkedForDeletion(self): return self._markedForDeletion def displayReloaded(self): self._reload_timer.start() self._extra_right = 25 self.update() def reloadDone(self): self._extra_right = 0 self.prepareGeometryChange() # this is required to clear the msg self.update() def initStateMachine(self): # NODE # Set up intial state graph. self._machine = GPI_FSM('NODE') self._switchSig.connect(self._machine.next) # node states self._idleState = GPIState('idle', self.idleRun, self._machine) self._chkInPortsState = GPIState('chkInPorts', self.chkInPortsRun, self._machine) self._computeState = GPIState('compute', self.computeRun, self._machine) self._post_compState = GPIState('post_compute', self.post_computeRun, self._machine) self._computeErrorState = GPIState('c_error', self.computeErrorRun, self._machine) self._validateError = GPIState('v_error', self.validateErrorRun, self._machine) self._initUIErrorState = GPIState('i_error', self.initUIErrorRun, self._machine) self._disabledState = GPIState('disabled', self.disabledRun, self._machine) # make state graph # idle self._idleState.addTransition('check', self._chkInPortsState) self._idleState.addTransition('disable', self._disabledState) #self._idleState.addTransition('init_error', self._computeErrorState) #self._idleState.addTransition('init_warn', self._validateError) # should this exist? self._idleState.addTransition('i_error', self._initUIErrorState) # chkInPorts self._chkInPortsState.addTransition('compute', self._computeState) self._chkInPortsState.addTransition('ignore', self._idleState) self._chkInPortsState.addTransition('v_error', self._validateError) self._chkInPortsState.addTransition('c_error', self._computeErrorState) self._chkInPortsState.addTransition('disable', self._disabledState) # compute self._computeState.addTransition('c_error', self._computeErrorState) self._computeState.addTransition('next', self._post_compState) self._computeState.addTransition('disable', self._disabledState) # post_compute self._post_compState.addTransition('finished', self._idleState) self._post_compState.addTransition('v_error', self._validateError) self._post_compState.addTransition('c_error', self._computeErrorState) self._post_compState.addTransition('disable', self._disabledState) # error - validate() failed self._validateError.addTransition('check', self._chkInPortsState) self._validateError.addTransition('disable', self._disabledState) # error - compute() failed self._computeErrorState.addTransition('check', self._chkInPortsState) self._computeErrorState.addTransition('disable', self._disabledState) # disabled self._disabledState.addTransition('enable', self._idleState) def start(self): # NODE if self.inIdleState(): log.debug("NODE(" + self.name + "): emit switchSig") self._switchSig.emit('check') # get out of idle elif self.inValidateErrorState(): log.debug("NODE(" + self.name + "): FROM WARNING STATE: emit switchSig") self._switchSig.emit('check') # get out of warning elif self.inComputeErrorState(): log.debug("NODE(" + self.name + "): FROM ERROR STATE: emit switchSig") self._switchSig.emit('check') # get out of error else: log.error("NODE(" + self.name + "): Can't start node outside of idleState, skipping...") log.error(" ->current state: " + str(self.getCurStateName())) # Function executed upon state change: def idleRun(self, sig): self.printCurState() self._curState.emit('Idle ('+str(sig)+')') self.forceUpdate_NodeUI() self.debounceUISignals(sig) def debounceUISignals(self, sig): if sig == 'finished' or sig == 'ignore' or sig == 'v_error' or sig == 'c_error': # from post_compute or failed check before allowing new UI signals # to be processed, require that the last signal was succesfully # processed. -This significantly cuts down the amount of # wdgEvents() emitted and prevents recursion limit errors. self._nodeIF.blockWdgSignals(False) # allow new UI signals to trigger # re-enter processing state don't go to processing if change is # from 'disabled' or 'init' states self.graph._switchSig.emit('next') def chkInPortsRun(self, sig): self.printCurState() self._curState.emit('Check ('+str(sig)+')') try: if self.inPortsAreValid(): log.debug(" ->compute") self._switchSig.emit('compute') else: log.debug(" ->ignore") self._switchSig.emit('ignore') except: log.error("chkInPortsRun(): Failed") #log.error(sys.exc_info()) log.error(traceback.format_exc()) log.error(" ->error") self._switchSig.emit('c_error') # computeRun() support def nextSigEmit(self, arg): # save the retcode value from the runtime code for post-compute self._returnCode = arg self._switchSig.emit('next') def errorSigEmit(self): self._switchSig.emit('c_error') def computeRun(self, sig): self.printCurState() self._curState.emit('Compute ('+str(sig)+')') try: self.forceUpdate_NodeUI() self.resetOutportStatus() # changes color, allows 'change' to be determined # setup a new functor for either GPI_THREAD, GPI_PROCESS, or GPI_APPLOOP # -QThreads have to be trashed anyway (not sure why, but they'll eventually # segfault). self.nodeCompute_thread = GPIFunctor(self) self.nodeCompute_thread.finished.connect(self.nextSigEmit) # -> post_computeRun() self.nodeCompute_thread.terminated.connect(self.errorSigEmit) self._progress_was_on = False self._progress_timer.start() self.prepareGeometryChange() # tell scene to update self.nodeCompute_thread.start() except: log.error("computeRun(): Failed") log.error(traceback.format_exc()) self._switchSig.emit('c_error') def post_computeRun(self, sig): self.printCurState() self._curState.emit('Post Compute ('+str(sig)+')') try: self._nodeIF.post_compute_widget_update() self.setWidgetOutports() self.updateToolTips() # for ports self.updateToolTip() # for node if Return.isComputeError(self._returnCode): log.error(Cl.FAIL+str(self.getName())+Cl.ESC+": compute() failed.") self._switchSig.emit('c_error') elif Return.isValidateError(self._returnCode): log.error(Cl.FAIL+str(self.getName())+Cl.ESC+": validate() failed.") self._switchSig.emit('v_error') else: log.info("post compute SUCCESS, nextSig") self._switchSig.emit('finished') # go to idle # Don't remove the thread!!!, over successive executions the same thread # address seems to get used and for some unidentifiable reason, the # last thread reference will delete THIS reference before post_computeRun() # can process the returnCode(). #self.nodeCompute_thread = None except: log.error("post_computeRun(): Failed\n"+str(traceback.format_exc())) self._switchSig.emit('c_error') self._progress_timer.stop() if self._progress_was_on: self._progress_done.start() def computeErrorRun(self, sig): self.printCurState() self.graph._switchSig.emit('pause') # move canvas to a paused state to let users fix the problem self._curState.emit('Compute Error ('+str(sig)+')') self.forceUpdate_NodeUI() self.debounceUISignals(sig) def validateErrorRun(self, sig): self.printCurState() self.graph._switchSig.emit('pause') # move canvas to a paused state to let users fix the problem self._curState.emit('Validate Error ('+str(sig)+')') self.forceUpdate_NodeUI() self.debounceUISignals(sig) def initUIErrorRun(self, sig): self.printCurState() self.graph._switchSig.emit('pause') # move canvas to a paused state to let users fix the problem self._curState.emit('InitUI Error ('+str(sig)+')') self.forceUpdate_NodeUI() self.debounceUISignals(sig) def disabledRun(self, sig): self._curState.emit('Disabled ('+str(sig)+')') self.printCurState() # State Checking: def getCurState(self): return self._machine.curState # return self._machine.configuration() def getCurStateName(self): '''return state names in a list of strings''' return self._machine.curStateName def printCurState(self): if manager.isDebug(): log.debug("---------------------- Current Node("+self.name+") State(s):" + self.getCurStateName()) def inIdleState(self): if self._idleState is self.getCurState(): return True return False def waitUntilIdle(self): # while not self.inIdleState(): QtGui.QApplication.processEvents() # allow gui to update def inComputeErrorState(self): if self._computeErrorState is self.getCurState(): return True return False def inInitUIErrorState(self): if self._initUIErrorState is self.getCurState(): return True return False def inValidateErrorState(self): if self._validateError is self.getCurState(): return True return False def isProcessingEvent(self): return (not self.inIdleState()) \ and (not self.inValidateErrorState()) \ and (not self.inComputeErrorState()) \ and (not self.inDisabledState()) \ and (not self.inInitUIErrorState()) def isReady(self): return self.hasEventPending() and (not self.inDisabledState()) and (not self.inInitUIErrorState()) def setDisabledState(self, val): if val is True: self._machine.next('disable') # self._switchSig.emit('disable') elif val is False: self._machine.next('enable') # self._switchSig.emit('enable') def inDisabledState(self): if self._disabledState is self.getCurState(): return True return False def progressON(self): wt = self.maxWallTime() # if there is no walltime then don't est progress if wt and self.nodeCompute_thread: # only do progress bars if the node time is more than a second if (wt > self._prog_thresh) and (self.nodeCompute_thread.curTime() > self._prog_thresh): return True return False def setEventStatus(self, val): '''None: to clear event status. {GPI_PORT_EVENT:'title'}: for the event type and port name. {GPI_WIDGET_EVENT:'title'}: for the event type and widget name. {GPI_INIT_EVENT:None}: if the node is freshly added. {GPI_REQUEUE_EVENT:None}: if the node was automatically requeued. ''' if val is not None: self._event_type = val # keep last event self._event_pending = True self.appendEvent(val) else: self._event_pending = False # the queue and delete functions set this to none # so do the handoff at this point to make it available # to the user. self._events_handoff = self._events self._events = EventManager() # make a new one def appendEvent(self, val): # translate to new obj if GPI_WIDGET_EVENT in val: self._events.addWidgetEvent(val[GPI_WIDGET_EVENT]) if GPI_PORT_EVENT in val: # TODO: for some reason, events are being added to _nodeIF at close # so check for valid _nodIF function before using. # -its b/c nodes are being deleted, which is causing more events. # -this hack takes care of it for now. if hasattr(self._nodeIF, 'getWidgetNames'): # re-map widget-port events to widget events. if val[GPI_PORT_EVENT] in self._nodeIF.getWidgetNames(): self._events.addWidgetEvent(val[GPI_PORT_EVENT]) else: self._events.addPortEvent(val[GPI_PORT_EVENT]) if GPI_INIT_EVENT in val: self._events.setInitEvent() if GPI_REQUEUE_EVENT in val: self._events.setRequeueEvent() def hasEventPending(self): return self._event_pending def getPendingEvent(self): '''Return copy of event to protect orig.''' return copy.deepcopy(self._event_type) def getPendingEvents(self): '''Return copy of event to protect orig.''' return copy.deepcopy(self._events_handoff) def getModuleName(self): return self._moduleName def setReQueue(self, val=False): # NODE # At the end of a nodeQueue, these tasked are checked for # more events. self._requeue = val def modifyWdg(self, title, kwargs): # NODE if self._nodeIF: # not deleted self._nodeIF.modifyWidget_direct(str(title), **kwargs) def updateToolTips(self): for port in self.getPorts(): port.updateToolTip() def getPortByID(self, pID): for port in self.getPorts(): if port.getID() == pID: return port log.error("getPortByID(): failed to find port id: " + str(pID)) def getPorts(self): return self.inportList + self.outportList def getCyclicPorts(self): plist = [] for port in self.inportList: if port.allowsCyclicConn(): plist.append(port) return plist def getNonCyclicPorts(self): plist = [] for port in self.getPorts(): if not port.allowsCyclicConn(): plist.append(port) return plist def getInPortByNumOrTitle(self, pnumORtitle): return self.getPortByNumOrTitle(pnumORtitle, self.inportList) def getOutPortByNumOrTitle(self, pnumORtitle): return self.getPortByNumOrTitle(pnumORtitle, self.outportList) def getPortByNumOrTitle(self, pnumORtitle, portList=None): if portList is None: portList = self.getPorts() if type(pnumORtitle) is int: if (pnumORtitle < 0) or (pnumORtitle >= len(portList)): log.error("getPortByNumOrTitle (from Node): target out of port range: \'" +stw(pnumORtitle) +"\'") return src = portList[pnumORtitle] elif type(pnumORtitle) is str: src = None cnt = 0 for port in portList: if port.portTitle == pnumORtitle: src = port pnumORtitle = cnt cnt += 1 if src is None: log.error("getPortByNumOrTitle(): failed to find port: \'" + stw(pnumORtitle)+"\'") return else: log.critical("getPortByNumOrTitle(): ERROR: port identifier must be either int or str") return return src def getInPort(self, pnumORtitle): return self.getPortByNumOrTitle(pnumORtitle, self.inportList) def getOutPort(self, pnumORtitle): return self.getPortByNumOrTitle(pnumORtitle, self.outportList) def getPos(self): return [self.scenePos().x(), self.scenePos().y()] def getSettings(self): # NODE SETTINGS '''list all settings required to re-instantiate this node ''' s = {} if self.item: # macro nodes won't have this item s['key'] = self.item.key() # library lookup key if self.curWallTime(): # if the node hasn't been run, then there is no timing s['walltime'] = str(self.curWallTime()) # in sec s['avgwalltime'] = str(self.avgWallTime()) s['stdwalltime'] = str(self.stdWallTime()) s['id'] = self.getID() # unique canvas id s['pos'] = self.getPos() s['name'] = self.getModuleName() s['widget_settings'] = copy.deepcopy(self._nodeIF.getSettings()) s['ports'] = [] for port in self.getPorts(): s['ports'].append(copy.deepcopy(port.getSettings())) return s def getWidgetAndPortNames(self): o = [] for p in self.getPorts(): o.append(p.getName()) for w in self._nodeIF.getWidgets(): o.append(str(w.title())) return o def setID(self, value=None): if value is None: self._id = id(self) # this will always be unique else: self._id = value def getID(self): return self._id def detachSelf(self): '''Remove all upstream and downstream connections''' # inports for port in self.inportList: self.detachPortByRef(port) # outports for port in self.outportList: self.detachPortByRef(port) def removePorts(self): '''Remove all connections, then delete the port objects''' self.detachSelf() for port in self.inportList: if port.scene(): self.graph.scene().removeItem(port) del self.inportList[:] for port in self.outportList: if port.scene(): self.graph.scene().removeItem(port) del self.outportList[:] def detachPortByRef(self, port): edges = list(port.edgeList) # since edgeList is modified by detachSelf for edge in edges: edge.detachSelf() if edge.scene(): self.graph.scene().removeItem(edge) # del edge def removePortByRef(self, port): self.detachPortByRef(port) try: self.inportList.remove(port) except: try: self.outportList.remove(port) except: log.critical("Port not found in either inportList or outportList.") if port.scene(): self.graph.scene().removeItem(port) def removeMenu(self): # close widgets if self._nodeIF: # macro safe for parm in self._nodeIF.parmList: try: if parm.parent() is self._nodeIF: # not sure if this protects against # c++ wrapper already deleted error parm.setParent(None) parm.close() except: log.error(str(inspect.currentframe().f_back.f_lineno)+" parm has likely been deleted. Skipping...") # close menu self._nodeIF.close() self._nodeIF = None if self._nodeIF_scrollArea: self._nodeIF_scrollArea = None def getParmList(self): return self._nodeIF.parmList def deleteComputeThread(self): if self.nodeCompute_thread: # it is currently running self.nodeCompute_thread.blockSignals(True) if self.nodeCompute_thread.isRunning(): log.debug("deleteComputeThread(): Node(" + self.name + \ "): Thread termination executed.") self.nodeCompute_thread.terminate() # die hard self.nodeCompute_thread = None # not sure if deleting is the right way #del self.nodeCompute_thread def removeMMAPs(self): # remove memmap file handles i = str(self.getID()) for p in os.listdir(GPI_SHDM_PATH): if p.endswith(i): os.remove(os.path.join(GPI_SHDM_PATH,p)) def readyForDeletion(self): self.setDeleteFlag(True) # Removes edges, ports, and menu before its removed from the scene self.setDisabledState(True) self.setEventStatus(None) self.removeMenu() self.removePorts() self.deleteComputeThread() self.removeMMAPs() # def hoverEnterEvent(self, event): # self.beingHovered = True # self.update() # # def hoverLeaveEvent(self, event): # self.beingHovered = False # self.update() def appendWallTime(self, time): '''Only keep the last 100 wall times. ''' self._computeDuration.append(time) if len(self._computeDuration) > 100: self._computeDuration.pop(0) def curWallTime(self): if len(self._computeDuration): return self._computeDuration[-1] def maxWallTime(self): # get the most recent time over 'thresh' if len(self._computeDuration): x = np.array(self._computeDuration) x = x[x > self._prog_thresh] if len(x) > 0: return x[-1] #return max(self._computeDuration) def avgWallTime(self): if len(self._computeDuration): return sum(self._computeDuration)/len(self._computeDuration) def stdWallTime(self): if len(self._computeDuration): avg = self.avgWallTime() # a little wastefull return math.sqrt(sum( [ (x-avg)**2 for x in self._computeDuration ] )/len(self._computeDuration)) def portMem(self): # a byte count of all outport memory being held bytes_held = 0 for port in self.outportList: # numpy arrays have a direct byte count if hasattr(port.data, 'nbytes'): bytes_held += port.data.nbytes # try to get an estimate of the object w/ sys else: bytes_held += sys.getsizeof(port.data) return bytes_held def updateToolTip(self): # NODE bytes_held = self.portMem() if Specs.TOTAL_PHYMEM() == 0: pct_physmem = "" else: pct_physmem = ", %.*f%s RAM" % (1, float( 100.0 * bytes_held / Specs.TOTAL_PHYMEM()), "%") # compute duration might not exist if node dies/fails to load. try: tip = 'Wall Time: ' + GetHumanReadable_time(self._computeDuration[-1]) + "\n" except: tip = '' if len(self._computeDuration): avg = self.avgWallTime() std = self.stdWallTime() tip += '\u03BC = ' + GetHumanReadable_time(avg) tip += ', \u03C3 = ' + GetHumanReadable_time(std) tip += ', n = ' + str(len(self._computeDuration)) + '\n' tip += 'Outport Mem: ' + GetHumanReadable_bytes( bytes_held) + pct_physmem self.setToolTip(tip) def setHierarchalLevel(self, level): self._hierarchal_level = level def getHierarchalLevel(self): return self._hierarchal_level def resetHierarchalLevel(self): self._hierarchal_level = -1 def menu(self): '''raises node menu.''' if not self._menuHasRaised: self._nodeIF_scrollArea.resize(self._nodeIF.sizeHint()) self._menuHasRaised = True self._nodeIF_scrollArea.show() self._nodeIF_scrollArea.raise_() self._nodeIF.activateWindow() def closemenu(self): '''closes node menu.''' if self._nodeIF_scrollArea: self._nodeIF_scrollArea.close() self._menuHasRaised = False def getModuleCompute(self): return self._nodeIF.compute def getModuleValidate(self): return self._nodeIF.validate def getNodeLabel(self): return self._nodeIF.label def forceUpdate_NodeUI(self): self._forceUpdate.emit() self.update() # only run this if execType is an APPLOOP if self._nodeIF.execType() == GPI_APPLOOP: QtGui.QApplication.processEvents() # allow gui to update def execType(self): return self._nodeIF.execType() def inPortsAreValid(self): # check that all required ports have data # and that the data matches the requested type dontRunFlag = False for inport in self.inportList: if inport.getUpstreamData() is None: if inport.isREQUIRED(): # skip compute routine and cleanup # don't send downstream events log.debug("nodeCompute(): inport is required, but empty, skipping compute()") dontRunFlag = True else: # check that the data match the port description if not inport.incomingDataTypeMatches(): # disconnect the port since the data, now doesn't match # or throw an error? Not sure which is better. edge = inport.edgeList[0] edge.detachSelf() if edge.scene(): self.graph.scene().removeItem(edge) log.debug("nodeCompute(): incoming data doesn't match, skipping compute()") dontRunFlag = True else: # there IS data and it DOES match: # set widget data if necessary if inport.isWidgetPort(): # print "nodeCompute(): "+inport.menuWidget.getTitle() + \ # " value is set to: " + str(inport.getUpstreamData()) inport.menuWidget.setValueQuietly( inport.getUpstreamData()) return (not dontRunFlag) def setWidgetOutports(self): # send events for widget ports for port in self.outportList: if port.isWidgetPort(): if port.setWidgetData(): port.setDownstreamEvents() port.update() def resetOutportStatus(self): for port in self.outportList: port.setDataCalled(False) port.update() def setData(self, pnumORtitle, data): '''Set output data, determine port status, and send downstream events''' port = self.getOutPort(pnumORtitle) port.setData(data) if port.dataHasChanged(): port.setDownstreamEvents() port.update() # allow gui update so port status can be seen # QtGui.QApplication.processEvents() def isTopNode(self): cnt = 0 for port in self.getPorts(): if isinstance(port, InPort): cnt += len(port.edges()) if cnt == 0: return True else: return False def getConnectionTuples(self): '''For each port (in and out) on this node list the connected nodes as a tuple indicating (src, sink) relationship. ''' c = [] for port in self.getPorts(): c += port.getConnectionTuples() return c def getNonCyclicConnectionTuples(self): '''For each port (in and out) on this node list the connected nodes as a tuple indicating (src, sink) relationship. ''' c = [] for port in self.getPorts(): c += port.getNonCyclicConnectionTuples() return c def edges(self): edges = [] for port in self.getPorts(): for edge in port.edges(): edges.append(edge) return(edges) def setName(self, name): self.name = name self.update() # update node appearance if self._nodeIF is not None: # update node menu self._nodeIF.updateTitle() def getName(self): # node title return self.name def getNameFromItem(self): return self.item.name def getFullPath(self): return self.item.fullpath def refreshName(self): '''Name based on node level and pending event status''' self.update() def titleExists(self, title): for port in self.getPorts(): if port.portTitle == title: return True # print type(self) if self._nodeIF: for wdg in self._nodeIF.parmList: if wdg.getTitle() == title: return True return False # Parse the extTypes dict held by the graph # to get an instance of the requested GPIType. def findGPIType(self, ptype): if self.graph: typ, req = self.graph._library.getType(ptype) if not req: log.warn(str(self._moduleName)+' NODE: Requested port-type: \''+str(ptype)+'\' not found. Using \'PASS\' instead.') return typ else: # this is just in case the node is being instantiated as a dummy return GPIDefaultType() def addInPort(self, title=None, ptype=None, obligation=REQUIRED, menuWidget=None, cyclic=False, **kwargs): # NODE # check if title is used (but not for wdg ports, # they are already checked). if self.titleExists(title) and (menuWidget is None): log.error("addInPort(): Port title \'" + str(title) \ + "\' is already in use! Aborting.") return # Parse kwargs for InPort and GPIType args. # Then send the appropriate args to GPIType() and InPort() # constructors respectively. # If no type can be found (including basic py-types), then # use the GPIDefaultType for a PASS port. type_cls = self.findGPIType(ptype) type_cls.setTypeParms(**kwargs) portNum = len(self.inportList) port = InPort(self, self.graph, title, portNum, intype=type_cls, obligation=obligation, menuWidget=menuWidget, cyclic=cyclic) port.setParentItem(self) port.setPosByPortNum(portNum) self.inportList.append(port) def addOutPort(self, title=None, ptype=None, obligation=REQUIRED, menuWidget=None, **kwargs): # NODE # check if title is used (but not for wdg ports, # they are already checked). if self.titleExists(title) and (menuWidget is None): log.error("addOutPort(): Port title \'" + str(title) \ + "\' is already in use! Aborting.") return # Parse kwargs for OutPort and GPIType args. # Then send the appropriate args to GPIType() and OutPort() # constructors respectively. # If no type can be found (including basic py-types), then # use the GPIDefaultType for a PASS port. type_cls = self.findGPIType(ptype) type_cls.setTypeParms(**kwargs) portNum = len(self.outportList) port = OutPort(self, self.graph, title, portNum, intype=type_cls, obligation=obligation, menuWidget=menuWidget) port.setParentItem(self) port.setPosByPortNum(portNum) self.outportList.append(port) def type(self): return Node.Type def calculateForces(self): if not self.scene() or self.scene().mouseGrabberItem() is self or self.isTopNode(): self.newPos = self.pos() return # Sum up all forces pushing this item away. xvel = 0.0 yvel = 0.0 for item in list(self.scene().items()): if not isinstance(item, Node): continue # don't let unattached nodes exert force if len(item.edges()) == 0: continue # get node distance line = QtCore.QLineF(self.mapFromItem( item, 0, 0), QtCore.QPointF(0, 0)) dx = line.dx() dy = line.dy() l = 2.0 * (dx * dx + dy * dy) if l > 0: xvel += (dx * 150.0) / l yvel += (dy * 150.0) / l # Now subtract all forces pulling items together. weight = (len(self.edges()) + 1) * 5.0 for edge in self.edges(): if edge.sourceNode() is self: pos = self.mapFromItem(edge.destNode(), 0, 0) else: pos = self.mapFromItem(edge.sourceNode(), 0, 0) xvel += pos.x() / weight yvel += pos.y() / weight # downward force yvel += 0.01 # friction if QtCore.qAbs(xvel) < 0.1 and QtCore.qAbs(yvel) < 0.1: xvel = yvel = 0.0 sceneRect = self.scene().sceneRect() self.newPos = self.pos() + QtCore.QPointF(xvel, yvel) self.newPos.setX(min(max( self.newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10)) self.newPos.setY(min(max( self.newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10)) def advance(self): if self.newPos == self.pos(): return False self.setPos(self.newPos) return True def getTitleSize(self): '''Determine how long the module box is.''' buf = self.name #if self._nodeIF: # if self._nodeIF.label != '': # buf += ": " + self._nodeIF.label fm = QtGui.QFontMetricsF(self.title_font) bw = fm.width(buf) + self._right_margin bh = fm.height() return (bw, bh) def getLabel(self): if self._nodeIF is None: return '' if self._nodeIF.getLabel() is not None: return self._nodeIF.getLabel() return '' def getLabelSize(self): '''Determine label width and height''' buf = '' if self._nodeIF is None: return (0.0, 0.0) if self._nodeIF.getLabel() != '': buf += self._nodeIF.getLabel()[:self._label_maxLen] else: return (0.0,0.0) fm = QtGui.QFontMetricsF(self._label_font) bw = fm.width(buf) + self._label_inset + self._right_margin bh = fm.height() return (bw, bh) def getDetailLabelSize(self): buf = '' if self._nodeIF is None: return (0.0, 0.0) if self._nodeIF.getDetailLabel() != '': buf += self._nodeIF.getDetailLabel() else: return (0.0,0.0) fm = QtGui.QFontMetricsF(self._detailLabel_font) tw = self.getTitleSize()[0] el_buf = fm.elidedText(self._nodeIF.getDetailLabel(), self._nodeIF.getDetailLabelElideMode(), tw * 3) bw = fm.width(el_buf) + self._detailLabel_inset + self._right_margin bh = fm.height() return (bw, bh) def getMaxPortWidth(self): '''Determine how long the module box is.''' l = max(len(self.inportList), len(self.outportList)) # from addInPort(): -8+8*portNum return l * 8.0 + 4.0 def updateOutportPosition(self): for o in self.outportList: o.resetPos() def getNodeWidth(self): return max(self.getMaxPortWidth(), self.getTitleSize()[0], self.getLabelSize()[0], self.getDetailLabelSize()[0]) def getNodeHeight(self): return self.getLabelSize()[1] + self.getTitleSize()[1] + self.getDetailLabelSize()[1] + self._bottom_margin def getProgressWidth(self): w = 23 conf = self.getCurState() if (self._computeState is conf) and self.progressON(): return w elif self._progress_done.isActive() and self._progress_was_on: return w return 0 def getExtraWidth(self): '''for adding info displays to the right of the node. ''' return self._extra_right def getOutPortVOffset(self): return self.getLabelSize()[1] + self.getDetailLabelSize()[1] + self._bottom_margin + 2 def shape(self): path = QtGui.QPainterPath() w = self.getNodeWidth() + self.getProgressWidth() + self.getExtraWidth() h = self.getNodeHeight() path.addRect(-10, -10, w, h) return path def boundingRect(self): adjust = 1.0 w = self.getNodeWidth() + self.getProgressWidth() + self.getExtraWidth() h = self.getNodeHeight() return QtCore.QRectF((-10 - adjust), (-10 - adjust), (w + 2*adjust), (h + 2*adjust)) def paint(self, painter, option, widget): # NODE # painter is a QPainter object w = self.getNodeWidth() h = self.getNodeHeight() # choose module color gradient = QtGui.QRadialGradient(-10, -10, 40) conf = self.getCurState() if self._computeState is conf: gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.gray).lighter(70)) gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.darkGray).lighter(70)) elif (option.state & QtGui.QStyle.State_Sunken) or (self._computeErrorState is conf): gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.red).lighter(150)) gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.red).lighter(170)) elif self._validateError is conf: gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.yellow).lighter(190)) gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.yellow).lighter(170)) elif self._initUIErrorState is conf: gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.red).lighter(150)) gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.yellow).lighter(170)) else: gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.gray).lighter(150)) gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.darkGray).lighter(150)) # draw module box (apply color) painter.setBrush(QtGui.QBrush(gradient)) if self.beingHovered or self.isSelected(): fade = QtGui.QColor(QtCore.Qt.red) fade.setAlpha(100) painter.setPen(QtGui.QPen(fade, 2)) else: #painter.setPen(QtGui.QPen(QtCore.Qt.black, 0)) #painter.setPen(QtCore.Qt.NoPen) fade = QtGui.QColor(QtCore.Qt.black) fade.setAlpha(50) painter.setPen(QtGui.QPen(fade,0)) # node body painter.drawRoundedRect(-10, -10, w, h, 3, 3) # title painter.setPen(QtGui.QPen(QtCore.Qt.black, 0)) painter.setFont(self.title_font) buf = self.name painter.drawText(-self._left_margin, -self._top_margin, w, self.getTitleSize()[1], (QtCore.Qt.AlignLeft), str(buf)) # label buf = '' if self._nodeIF: if self._nodeIF.getLabel() != '': buf += self._nodeIF.getLabel()[:self._label_maxLen] th = self.getTitleSize()[1] gr = QtGui.QColor(QtCore.Qt.black) gr.setAlpha(175) painter.setPen(QtGui.QPen(gr, 0)) painter.setFont(self._label_font) painter.drawText(self._label_inset-self._left_margin, -self._top_margin+th, w, self.getLabelSize()[1], (QtCore.Qt.AlignLeft), str(buf)) # detail label (aka node text) if self._nodeIF: if self._nodeIF.getDetailLabel() != '': fm = QtGui.QFontMetricsF(self._detailLabel_font) # elided text will shorten the string, adding '...' where # characterss are removed tw, th = self.getTitleSize() el_buf = fm.elidedText(self._nodeIF.getDetailLabel(), self._nodeIF.getDetailLabelElideMode(), tw * 3) if self.getLabelSize()[1]: th += self.getLabelSize()[1] gr = QtGui.QColor(QtCore.Qt.black) gr.setAlpha(150) painter.setPen(QtGui.QPen(gr, 0)) painter.setFont(self._detailLabel_font) painter.drawText(self._detailLabel_inset-self._left_margin, -self._top_margin+th, w, self.getDetailLabelSize()[1], (QtCore.Qt.AlignLeft), str(el_buf)) # reloaded disp if self._reload_timer.isActive() and not self.progressON(): self.drawReload(painter) # progress disp if (self._computeState is conf) and self.progressON(): wt = self.maxWallTime() pdone = self.nodeCompute_thread.curTime()/wt self._progress_was_on = True # normal counter if pdone < 1: self.drawProgress(painter, pdone) # recalculate else: self.drawRecalculating(painter) # force it to show 100% for a little longer elif self._progress_done.isActive() and self._progress_was_on: # clock circle pdone = 1 self.drawProgress(painter, pdone) def drawProgress(self, painter, pdone): # color fade = QtGui.QColor(QtCore.Qt.black) fade.setAlpha(200) # clock circle rect = QtCore.QRectF(-8.0+self.getNodeWidth(), -10, 10.0, 10.0) r_inner = QtCore.QRectF(-7.0+self.getNodeWidth(), -9, 8.0, 8.0) # clock frame startAngle = 0 * 16 spanAngle = -16 * 360 lightgray = QtGui.QColor(QtCore.Qt.gray).lighter(120) lightgray.setAlpha(200) painter.setPen(QtGui.QPen(lightgray, 0, QtCore.Qt.SolidLine, QtCore.Qt.SquareCap, QtCore.Qt.RoundJoin)) painter.drawArc(r_inner, startAngle, spanAngle) # progress startAngle = 90 * 16 spanAngle = -16 * 360 * pdone painter.setPen(QtGui.QPen(fade, 2.0, QtCore.Qt.SolidLine, QtCore.Qt.SquareCap, QtCore.Qt.RoundJoin)) painter.drawArc(rect, startAngle, spanAngle) def drawRecalculating(self, painter): # color fade = QtGui.QColor(QtCore.Qt.black) #.lighter(140) fade.setAlpha(200) # arcs 1 rect = QtCore.QRectF(-8.0+self.getNodeWidth(), -10, 10.0, 10.0) startAngle = (( 0 - self._progress_recalculate) % 360) * 16 spanAngle = 90 * 16 painter.setPen(QtGui.QPen(fade, 2.0, QtCore.Qt.SolidLine, QtCore.Qt.SquareCap, QtCore.Qt.RoundJoin)) painter.drawArc(rect, startAngle, spanAngle) startAngle = ((180 - self._progress_recalculate) % 360) * 16 spanAngle = 90 * 16 painter.drawArc(rect, startAngle, spanAngle) self._progress_recalculate = (self._progress_recalculate + 13) % 360 # arcs 2 - inner if False: painter.setPen(QtGui.QPen(QtCore.Qt.red, 0.25)) fugde = 2 rect = QtCore.QRectF(-8.0+self.getNodeWidth()+fugde, -10+fugde, 20.0-fugde*2, 20.0-fugde*2) startAngle = (( 0 - self._progress_recalculate2) % 360) * 16 spanAngle = 90 * 16 painter.drawArc(rect, startAngle, spanAngle) startAngle = ((180 - self._progress_recalculate2) % 360) * 16 spanAngle = 90 * 16 painter.drawArc(rect, startAngle, spanAngle) self._progress_recalculate2 = (self._progress_recalculate2 + 11) % 360 # arcs 3 - inner inner if False: painter.setPen(QtGui.QPen(QtCore.Qt.darkGreen, 0.25)) fugde = 5 rect = QtCore.QRectF(-8.0+self.getNodeWidth()+fugde, -10+fugde, 20.0-fugde*2, 20.0-fugde*2) startAngle = (( 0 - self._progress_recalculate3) % 360) * 16 spanAngle = 90 * 16 painter.drawArc(rect, startAngle, spanAngle) startAngle = ((180 - self._progress_recalculate3) % 360) * 16 spanAngle = 90 * 16 painter.drawArc(rect, startAngle, spanAngle) self._progress_recalculate3 = (self._progress_recalculate3 + 7) % 360 def drawReload(self, painter): # color fade = QtGui.QColor(QtCore.Qt.black) fade.setAlpha(200) # clock circle rect = QtCore.QRectF(-8.0+self.getNodeWidth(), -10, 10.0, 10.0) startAngle = 180 * 16 spanAngle = -16 * 270 painter.setPen(QtGui.QPen(fade, 2.0)) painter.drawArc(rect, startAngle, spanAngle) painter.setBrush(fade) w = self.getNodeWidth() - 7.5 h = 0 self._arrow = [[w, h], [w+3.5, h-3.0], [w+3.5, h+3]] self._arrowShape = QtGui.QPolygonF() for i in self._arrow: self._arrowShape.append(QtCore.QPointF(i[0], i[1])) painter.setPen(QtCore.Qt.NoPen) painter.drawPolygon(self._arrowShape) def itemChange(self, change, value): if change == QtGui.QGraphicsItem.ItemPositionHasChanged: for port in self.getPorts(): port.updateEdges() self.graph.itemMoved() # charge-rep self.graph.viewAndSceneForcedUpdate() return super(Node, self).itemChange(change, value) def mousePressEvent(self, event): # NODE printMouseEvent(event) modifiers = getKeyboardModifiers() self.update() # update node color modmidbutton_event = (event.button() == QtCore.Qt.LeftButton and modifiers == QtCore.Qt.AltModifier) if event.button() == QtCore.Qt.MidButton or modmidbutton_event: event.accept() elif event.button() == QtCore.Qt.LeftButton: event.accept() # this has to accept to be moved elif event.button() == QtCore.Qt.RightButton: event.accept() else: event.ignore() super(Node, self).mousePressEvent(event) def mouseReleaseEvent(self, event): # NODE printMouseEvent(event) modifiers = getKeyboardModifiers() self.update() modrightbutton_event = ((event.button() == QtCore.Qt.RightButton) and (modifiers == QtCore.Qt.ControlModifier)) if event.button() == QtCore.Qt.MidButton: event.accept() elif event.button() == QtCore.Qt.LeftButton: event.accept() # this has to accept to be moved elif modrightbutton_event: # OSX users set their launchctl associated file prefs. if Specs.inOSX(): if self.getNodeDefinitionPath(): #subprocess.call(["open " + self.getNodeDefinitionPath()], shell=True) subprocess.Popen("open \"" + self.getNodeDefinitionPath() + "\"", shell=True) else: log.warn('No external module definition found, aborting...') # Linux users set their editor choice # TODO: this should be moved to config elif Specs.inLinux(): editor = 'gedit' if "EDITOR" in os.environ: editor = os.environ["EDITOR"] if self.getNodeDefinitionPath(): subprocess.Popen(editor + " \"" + self.getNodeDefinitionPath() + "\"", shell=True) else: log.warn('No external module definition file (.py) found, aborting...') else: log.warn('The Quick-Edit feature is not available for this OS, aborting...') elif event.button() == QtCore.Qt.RightButton: event.accept() self.menu() self.graph.scene().makeOnlyTheseNodesSelected([self]) else: event.ignore() super(Node, self).mouseReleaseEvent(event)
# def mouseDoubleClickEvent(self, event): # print "double-clicked a node" # event.accept()