Source code for gpi.canvasGraph
# 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 gc
import os
import sys
import copy
import math
import time
import random
# gpi
import gpi
from gpi import QtCore, QtGui
from .associate import Bindings, isGPIAssociatedFile, isGPIAssociatedExt
from .canvasScene import CanvasScene
from .cmd import Commands
from .defines import GPI_REQUEUE_EVENT, GPI_INIT_EVENT, GPI_WIDGET_EVENT
from .defines import getKeyboardModifiers, printMouseEvent, stw
from .defines import isMacroChildNode
from .defines import GetHumanReadable_bytes, GPI_APPLOOP, GetHumanReadable_time
from .defines import isGPINetworkFile, isGPIModFile
from .edge import Edge
from .layoutWindow import LayoutMaster
from .library import Library, NodeCatalogItem
from .macroNode import MacroNode
from .network import Network
from .node import Node
from .nodeQueue import GPINodeQueue
from .port import Port, InPort
from .stateMachine import GPI_FSM, GPIState
from . import topsort
from .logger import manager
# start logger for this module
log = manager.getLogger(__name__)
[docs]class GraphWidget(QtGui.QGraphicsView):
'''Provides the main canvas widget and background painting as well as the
execution model for the canvas.'''
changed = gpi.Signal(QtCore.QMimeData)
_switchSig = gpi.Signal(str)
_switchSig_info = gpi.Signal(dict)
_curState = gpi.Signal(dict)
def __init__(self, title, parent):
super(GraphWidget, self).__init__()
# a link to the main window
self.parent = parent
self._title = title
self._macroModule = False
# canvas info
self._starttime = 0
self._walltime = 0 # time between idle states
# node animation
self._node_anim_timeline = None
self._node_anims = []
self._layoutwindowList = []
self._proc = None # reference point for threads
self.timerId = 0
self.nodeEvent_timerId = self.startTimer(1000) # update time (msec)
self.chargeRepON = False # start off this way
scene = CanvasScene(self)
scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
ncscale = 4 # Network Canvas Size Scale
scene.setSceneRect(
-200 * ncscale, -200 * ncscale, 400 * ncscale, 400 * ncscale)
self.setScene(scene)
self.setCacheMode(QtGui.QGraphicsView.CacheNone) # required for repainting background
self.setViewportUpdateMode(
QtGui.QGraphicsView.BoundingRectViewportUpdate)
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
self.setInteractive(True)
self.scale(2.0, 2.0)
self.setMinimumSize(400, 400)
self.setDragMode(self.ScrollHandDrag)
self._panning = False
self.setAcceptDrops(True)
self.setCursor(QtCore.Qt.OpenHandCursor)
self.gridRes = 5 # pts
self.nodeQueue = GPINodeQueue()
self.extWidgets = dict()
# timed painter update
self._timer = QtCore.QTimer()
self._timer.timeout.connect(self.viewAndSceneForcedUpdate)
# self._timer.start(1000) # 10msec update
# TODO: this probably should go to the MainCanvas
self._library = Library(self)
#self._library.scanGPIModulesIn_SysPath(recursion_depth=2)
#self._library.generateLibMenus()
self._event_pos = QtCore.QPoint(0, 0)
self._network = Network(self)
self._pause_quiet = False
self.initStateMachine()
def rescanLibrary(self):
self._library.scanForNewNodes()
def getLibrary(self):
return self._library
def getEventPos(self):
return self._event_pos
def getEventPos_randomDev(self, rad=None):
if rad:
radius = rad
else:
radius = 10.0 # pts
x = self._event_pos.x() + random.random() * radius
y = self._event_pos.y() + random.random() * radius
pos = QtCore.QPoint(x, y)
pos = self.mapToScene(pos)
return pos
def title(self):
return self._title
def initStateMachine(self): # GRAPH
# Set up intial state graph.
self._machine = GPI_FSM('GRAPH')
self._switchSig.connect(self._machine.next)
self._switchSig_info.connect(self._machine.next)
# node states
self._undefinedStateSig = {'title':self.title(), 'msg':'Undefined State (how did you get here?)'}
self._initState = GPIState('init', self.initRun, self._machine, efunc=self.initWalltime)
self._idleState = GPIState('idle', self.idleRun, self._machine, efunc=self.initWalltime)
self._idleStateSig = {'title':self.title(), 'msg':'Idle'}
self._checkEventsState = GPIState('checkEvents', self.checkEventsRun, self._machine)
self._checkEventsStateSig = {'title':self.title(), 'msg':'Checking Events'}
#self._deleteNodeState = GPIState('deleteNode', self.deleteNodeRun,
# self._machine)
#self._addNodeState = GPIState('addNode', self.addNodeRun,
# self._machine)
self._processingState = GPIState('processing', self.processingRun,
self._machine)
self._processingStateSig = {'title':self.title(), 'msg':'Processing'}
self._pausedState = GPIState('paused', self.pausedRun, self._machine, efunc=self.pausedLeave)
self._pausedStateSig = {'title':self.title(), 'msg':'Paused'}
# make state graph
# init
self._initState.addTransition('init_check', self._checkEventsState)
self._initState.addTransition('init_finished', self._idleState)
self._initState.addTransition('pause', self._pausedState)
#self._initState.exited.connect(self.initWalltime)
# idle
self._idleState.addTransition('check', self._checkEventsState)
#self._idleState.addTransition('delete', self._deleteNodeState)
#self._idleState.addTransition('deleteAll', self._deleteNodeState)
#self._idleState.addTransition('load', self._addNodeState)
self._idleState.addTransition('pause', self._pausedState)
#self._idleState.exited.connect(self.initWalltime)
# checkEvents
self._checkEventsState.addTransition('process', self._processingState)
self._checkEventsState.addTransition('requeue', self._checkEventsState)
#self._checkEventsState.addTransition('delete', self._deleteNodeState)
#self._checkEventsState.addTransition('deleteAll',
# self._deleteNodeState)
self._checkEventsState.addTransition('ignore', self._idleState)
#self._checkEventsState.addTransition('load', self._addNodeState)
self._checkEventsState.addTransition('pause', self._pausedState)
# deleteNode
#self._deleteNodeState.addTransition('check', self._checkEventsState)
#self._deleteNodeState.addTransition('process', self._processingState)
# addNode
#self._addNodeState.addTransition('check', self._checkEventsState)
#self._addNodeState.addTransition('afterload', self._processingState)
# processing
#self._processingState.addTransition('delete', self._deleteNodeState)
#self._processingState.addTransition('deleteAll', self._deleteNodeState)
self._processingState.addTransition('pause', self._pausedState)
self._processingState.addTransition('check', self._checkEventsState)
self._processingState.addTransition('next', self._processingState)
#self._processingState.addTransition('load', self._addNodeState)
# pause
self._pausedState.addTransition('unpause', self._checkEventsState)
#self._machine.start(self._idleState)
self._machine.start(self._initState)
def walltime(self):
return self._walltime
def clearWalltime(self):
if 'walltime' in self._idleStateSig:
self._idleStateSig.pop('walltime')
def initWalltime(self, sig):
# sig is a dummy so that it can be an onExit state transition
self._starttime = time.time()
def calcWalltime(self):
self._walltime = time.time() - self._starttime
def walltime_disp(self):
return GetHumanReadable_time(self.walltime(), precision=1)
def initRun(self, sig):
# run any initialization stuff here
# since the 'check state' can't run yet, the canvas is virtually paused.
if Commands.pendingCount():
# load networks
if Commands.netCount():
for path in Commands.nets():
pos = self.getEventPos_randomDev(rad=50)
pos = QtCore.QPoint(pos.x(), pos.y())
s = {'sig': 'load', 'subsig': 'net', 'path':
path, 'pos': pos}
self.addNodeRun(s)
# load nodes
if Commands.modCount():
for path in Commands.mods():
pos = self.getEventPos_randomDev(rad=50)
pos = QtCore.QPoint(pos.x(), pos.y())
s = {'sig': 'load', 'subsig': 'mod',
'path': path, 'pos': pos, 'from': 'cmd.Commands'}
self.addNodeRun(s)
# load associated files
if Commands.fileCount():
for path in Commands.files():
pos = self.getEventPos_randomDev(rad=50)
pos = QtCore.QPoint(pos.x(), pos.y())
bpath, file_ext = os.path.splitext(path)
s = {'sig': 'load', 'subsig': file_ext, 'path': path, 'pos': pos}
self.addNodeRun(s)
# NOTE: macro-nodes that need to close, re-select themselves
# -so this call doesn't work on them
self.scene().unselectAllItems()
# once all networks are loaded, process node arguments
# String-Node Args
if Commands.stringNodeArgCount():
for lab in Commands.stringNodeLabels():
node = self.findNodeByNameAndLabel('String', lab)
if node:
# get the string arg
arg = Commands.stringNodeArg(lab)
# set 'string' widget value
node._nodeIF.modifyWidget_direct('string', val=arg)
node.setEventStatus({GPI_WIDGET_EVENT: 'string'})
else:
log.warn('String node label: \''+str(lab)+'\' not found, skipping.')
self._switchSig.emit('init_check')
else:
self._switchSig.emit('init_finished')
def totalPortMem(self):
bytes_held = 0
for node in self.getAllNodes():
bytes_held += node.portMem()
return bytes_held
def totalPortMem_disp(self, bytes_held):
return 'Total Port MEM: '+GetHumanReadable_bytes(bytes_held)
# Function executed upon state change:
def idleRun(self, sig):
# get walltime and put it here
self.calcWalltime()
if self.walltime() > 0:
self._idleStateSig['walltime'] = self.walltime_disp()
else:
self.clearWalltime()
self._curState.emit(self._idleStateSig)
self.printCurState()
self.viewAndSceneForcedUpdate()
# idle is a good time to force collection
log.debug('pausedRun(): garbage collect')
gc.collect()
# if GPI was started without GUI, then assume the network has finished and exit
if Commands.noGUI() or Commands.scriptMode():
self.deleteAllNodeMMAPs()
log.dialog('Canvas Wall Time: '+str(self.walltime_disp()) + ', exiting.')
sys.exit(0)
def pausedRun(self, sig):
self._curState.emit(self._pausedStateSig) # update statusbar
self.printCurState()
# don't draw yellow bkgnd
if 'subsig' in sig:
self._pause_quiet = True
self.viewAndSceneForcedUpdate()
# pause is a good time to force collection
log.debug('pausedRun(): garbage collect')
gc.collect()
# if GPI was started without GUI, then assume the network has finished and exit
if Commands.noGUI() or Commands.scriptMode():
self.deleteAllNodeMMAPs()
log.dialog('The canvas fell into a paused state, exiting.')
sys.exit(1)
def pausedLeave(self, sig):
# always reset quiet flag
self._pause_quiet = False
def checkEventsRun(self, sig):
self._curState.emit(self._checkEventsStateSig)
self.printCurState()
# Currently Running nodes
if self.aNodeIsProcessing():
self._switchSig.emit('process')
return
# EVENTS
# check for event status BEFORE triggering highest compute
for node in self.getAllNodes():
if node.isReady():
# Re/-initialize queue and start processing.
# This was called because 'a' node has an event status.
self.nodeQueue.setQueue(self.getLinearNodeHierarchy())
self._switchSig.emit('process')
return
# REQUEUE EVENTS
# if queue is done then check for re-queue nodes
if self.nodeQueue.isEmpty():
log.debug("checkEventsRun(): check for requeue nodes.")
nodes = self.getAllNodes()
cnt = 0
for node in nodes:
if node._nodeIF: # protect against deleted object
if node._nodeIF.reQueueIsSet() and \
not node.inDisabledState():
node.setEventStatus({GPI_REQUEUE_EVENT: None})
cnt += 1
if cnt: # if any nodes got reset then start loop
self._switchSig.emit('requeue')
return
# NO EVENTS
# else: no events or requeue events were found
self._switchSig.emit('ignore')
def newNode_byClosestMatch(self, name, wdg_port_names, pos, mapit=False):
# Search the library for all nodes with the same name, then do a
# sub-search based on a list of wdg names.
item = self._library.findNode_byClosestMatch(name, wdg_port_names)
if item:
item.reload()
log.debug('\tfound')
return self.newNode_byNodeCatalogItem(item, pos, mapit)
else:
log.debug('\tfailed to find node')
def newNode_byKey(self, key, pos, mapit=False):
# search the library for a node with given name
item = self._library.findNode_byKey(key)
if item:
item.reload()
log.debug('\tfound')
return self.newNode_byNodeCatalogItem(item, pos, mapit)
else:
log.debug('\tfailed to find node')
def newNode_byName(self, name, pos, mapit=False):
# search the library for a node with given name
item = self._library.findNode_byName(name)
if item:
item.reload()
log.debug('\tfound')
return self.newNode_byNodeCatalogItem(item, pos, mapit)
else:
log.debug('\tfailed to find node')
def newNode_byPath(self, path, pos, mapit=False):
# just try to make a node item, if it loaded then its valid.
item = NodeCatalogItem(path)
item.load()
if item.valid():
log.debug('\tsuccess')
return self.newNode_byNodeCatalogItem(item, pos, mapit)
else:
log.debug('\titem cannot be loaded')
def newNode_byNodeCatalogItem(self, item, pos, mapit=False):
'''Add a new node to the canvas from a NodeCatalogItem description.
Return a handle to the new canvas item.
pos: QtCore.QPoint()
'''
if item is None:
return None
# Update user modifications (if any).
item.reload()
# If the user has made changes that cause the node to be non-loadable
# then return None
if not item.valid():
return None
newnode = Node(self, nodeCatItem=item)
# force all execType(s) to be GPI_APPLOOP
if False: # letting them all be processes seems to be the safest for now
#if Commands.noGUI():
# Thread seems safer, APPLOOP was causing recursion errors.
# Probably due to signals piling up.
# GPI_APPLOOP & iter_test.net causes: recursion error
et = lambda :GPI_APPLOOP
# GPI_THREAD & beta_spiral.net causes: 64119 Bus error: 10
#et = lambda :GPI_THREAD
newnode.execType = et
newnode._nodeIF.execType = et
newnode.refreshName()
self.scene().addItem(newnode)
if mapit:
mpos = self.mapToScene(pos)
else:
mpos = pos
newnode.setPos(mpos.x(), mpos.y())
return newnode
# TODO: since addNodeRun was removed from the state-machine it now needs
# a real function interface instead of passing a dict to parameterize
def addNodeRun(self, sig): # state: 'addNode', 'Run' method
self.printCurState()
if type(sig['subsig']) == NodeCatalogItem:
log.debug('addNode by item')
item = sig['subsig']
# get the position of menu invocation
radius = 10.0 # pts
x = self._event_pos.x() + random.random() * radius
y = self._event_pos.y() + random.random() * radius
pos = QtCore.QPoint(x, y)
# instantiate node on canvas
node = self.newNode_byNodeCatalogItem(item, pos, mapit=True)
if node:
self.scene().makeOnlyTheseNodesSelected([node])
node.setEventStatus({GPI_INIT_EVENT: None})
self.ensureVisible(node)
elif sig['subsig'] == 'mod':
log.debug('addNode by path')
path = sig['path']
pos = sig['pos']
# instantiate node on canvas
node = self.newNode_byPath(path, pos, mapit=True)
if node:
self.scene().makeOnlyTheseNodesSelected([node])
node.setEventStatus({GPI_INIT_EVENT: None})
self.ensureVisible(node)
# 3-4 pieces of info for file associations
# node-name (and possibly key), file ext, string widget to push to
elif isGPIAssociatedExt(sig['subsig']):
# get binding for this file extension
# all extensions should be case-insensitive
item = Bindings.get(sig['subsig'].lower())
# assume the item is holding a full key
node = self.newNode_byKey(item.node, sig['pos'], mapit=True)
if node is None:
node = self.newNode_byName(item.node, sig['pos'], mapit=True)
if node is None:
log.error('\''+str(item.node)+'\' could not be located for \''+str(item.ext)+'\'')
else:
node._nodeIF.modifyWidget_direct(item.wdg, val=sig['path'])
self.scene().unselectAllItems()
node.setSelected(True)
node.setEventStatus({GPI_WIDGET_EVENT: item.wdg})
self.ensureVisible(node)
elif sig['subsig'] == 'net':
if 'pos' in sig:
net = self._network.loadNetworkFromFile(sig['path'])
if net:
self.deserializeCanvas(net, sig['pos'])
else:
net = self._network.loadNetworkFromFile(sig['path'])
if net:
self.deserializeCanvas(net, self.getEventPos_randomDev())
elif sig['subsig'] == 'dialog':
if 'pos' in sig:
net = self._network.loadNetworkFromFileDialog()
if net:
self.deserializeCanvas(net, sig['pos'])
else:
net = self._network.loadNetworkFromFileDialog()
if net:
self.deserializeCanvas(net, self.getEventPos_randomDev())
elif sig['subsig'] == 'paste':
if self.parent._copybuffer:
self.deserializeGraphData(self.parent._copybuffer, pos=sig['pos'])
elif sig['subsig'] == 'keypaste':
if self.parent._copybuffer:
self.deserializeGraphData(self.parent._copybuffer, offset=True, randoffset=True)
elif sig['subsig'] == 'reload':
if self.parent._copybuffer:
self.deserializeGraphData(self.parent._copybuffer, reloadnode=True)
if self.inIdleState():# or self.inCheckEventsState():
self._switchSig.emit('check')
def deleteNodeRun(self, sig):
self.printCurState()
if sig == 'delete': # delete selected
self.deleteSelectedNodes()
elif sig == 'deleteAll': # clear all
self.deleteAllNodes()
# back to processing or check for new events
#if self.nodeQueue.isEmpty():
# self._switchSig.emit('check')
#else:
# self._switchSig.emit('process')
if self.inIdleState():
self._switchSig.emit('check')
def aNodeIsProcessing(self):
for node in self.getAllNodes():
if node.isProcessingEvent():
return True
return False
def processingRun(self, sig):
self._curState.emit(self._processingStateSig)
self.printCurState()
if not self.aNodeIsProcessing():
queueState = self.nodeQueue.startNextNode()
if queueState == 'paused':
self._switchSig.emit('paused')
elif queueState == 'finished':
self._switchSig.emit('check')
self.viewAndSceneForcedUpdate()
# State Checking:
def getCurState(self):
return self._machine.curState
def getCurStateName(self):
'''return state names in a list of strings'''
return self._machine.curStateName
def getCurStateSig(self):
if self.inIdleState():
return self._idleStateSig
elif self.inPausedState():
return self._pausedStateSig
elif self.inCheckEventsState():
return self._checkEventsStateSig
elif self.inProcessingState():
return self._processingStateSig
else:
return self._undefinedStateSig
def printCurState(self):
log.debug("GRAPH State(s): "+self.getCurStateName())
def inIdleState(self): # GRAPH
return self._idleState is self.getCurState()
def inPausedState(self): # GRAPH
return self._pausedState is self.getCurState()
def inCheckEventsState(self):
return self._checkEventsState is self.getCurState()
def inProcessingState(self):
return self._processingState is self.getCurState()
def printNodeState(self):
allItems = self.getAllNodes()
for node in allItems:
print("________________________")
node.printCurState()
print(("node: " + str(node.name)))
print(("inDisabledState: " + str(node.inDisabledState())))
print(("hasEventPending: " + str(node.hasEventPending())))
print(("_nodeIF.reQueueIsSet: " + str(node._nodeIF.reQueueIsSet())))
print("________________________")
def setPauseState(self, val):
old_val = self.nodeQueue.isPaused()
self.nodeQueue.setPause(val)
if val != old_val: # state changed
if not val: # unpaused
# after pause drop old event queue
self.nodeQueue.resetQueue()
self._switchSig.emit('check')
def isPaused(self):
return self.nodeQueue.isPaused()
def scrollContentsBy(self, x, y):
super(GraphWidget, self).scrollContentsBy(x, y)
y = self.geometry().height() / 2
x = self.geometry().width() / 2
self._event_pos = QtCore.QPoint(x, y)
def newLayoutWindowFromSettings(self, s, nodeList):
# config has to be set at construction b/c layouts cannot yet be
# deleted.
layoutwindow = LayoutMaster(self, config=s['config'])
layoutwindow.loadSettings(s, nodeList)
layoutwindow.setWindowTitle(self._title+".Layout Window "+str(len(self._layoutwindowList)+1))
#scrollArea = QtGui.QScrollArea()
#scrollArea.setWidget(layoutwindow)
#scrollArea.setWidgetResizable(True)
#scrollArea.setGeometry(50, 50, 300, 1000)
#self._layoutwindowList.append(scrollArea)
self._layoutwindowList.append(layoutwindow)
layoutwindow.setGeometry(50, 50, 400, 300)
#scrollArea.show()
#scrollArea.raise_()
layoutwindow.show()
layoutwindow.raise_()
def newLayoutWindow(self, config):
layoutwindow = LayoutMaster(self, config=config)
layoutwindow.setWindowTitle(self._title+".Layout Window "+str(len(self._layoutwindowList)+1))
#scrollArea = QtGui.QScrollArea()
#scrollArea.setWidget(layoutwindow)
#scrollArea.setWidgetResizable(True)
#scrollArea.setGeometry(50, 50, 300, 1000)
#self._layoutwindowList.append(scrollArea)
self._layoutwindowList.append(layoutwindow)
layoutwindow.setGeometry(50, 50, 400, 300)
#scrollArea.show()
#scrollArea.raise_()
layoutwindow.show()
layoutwindow.raise_()
def serializeLayoutWindows(self):
'''Save each layout dict.
'''
s = []
for layout in self._layoutwindowList:
# layout is None for past closed windows.
# this keeps numbering correct
if layout is not None:
s.append(layout.getSettings())
return s
def dragEnterEvent(self, event):
if event.mimeData().hasFormat('text/uri-list'):
event.acceptProposedAction()
self.changed.emit(event.mimeData())
def dragMoveEvent(self, event):
if event.mimeData().hasFormat('text/uri-list'):
event.acceptProposedAction()
def dropEvent(self, event):
if event.mimeData().hasFormat('application/gpi-widget'):
mime = event.mimeData()
itemData = mime.data('application/gpi-widget')
dataStream = QtCore.QDataStream(
itemData, QtCore.QIODevice.ReadOnly)
text = QtCore.QByteArray()
offset = QtCore.QPoint()
dataStream >> text >> offset
log.debug("canvasGraph(): Mime data:")
log.debug(str(text))
log.debug(str(offset))
return
elif event.mimeData().hasFormat('text/uri-list'):
mimeData = event.mimeData()
log.debug(str(mimeData))
paths = [str(x.path()) for x in mimeData.urls()]
log.debug(paths)
# if multiple drops, then add random offsets to pos
if len(paths) == 1:
poses = [event.pos()]
else:
poses = []
for path in paths:
m = 50
rand = QtCore.QPoint(random.random()*m, random.random()*m)
poses.append(event.pos() + rand)
# process each dropped path
for path, pos in zip(paths, poses):
log.debug('Dropped uri: '+str(path))
# node definitions
if isGPIModFile(path):
# add the node to the library (and menu) if possible
item = NodeCatalogItem(path)
ret = self._library.addNode(item)
if ret > 0:
log.dialog('Added dropped node to the library.')
self._library.regenerateLibMenus()
elif ret == 0:
log.dialog('Dropped Node is already in the library.')
else:
log.error('Dropped Node is Invalid.')
return
# add the node to the canvas
s = {'sig': 'load', 'subsig': 'mod',
'path': path, 'pos': pos, 'from': 'Dropped uri'}
self.addNodeRun(s)
# file associations
elif isGPIAssociatedFile(path):
bpath, file_ext = os.path.splitext(path)
s = {'sig': 'load', 'subsig': file_ext, 'path': path, 'pos': pos}
self.addNodeRun(s)
# network files
elif isGPINetworkFile(path):
s = {'sig': 'load', 'subsig': 'net', 'path':
path, 'pos': self.mapToScene(pos)}
self.addNodeRun(s)
# not a recognized file
else:
log.warn("dropEvent(): Filetype not recognized by GPI.")
return
# shows rejected animation if not called
event.acceptProposedAction()
def dragLeaveEvent(self, event):
event.accept()
def itemMoved(self):
# only start this timer if the menu option for CR is set
if self.chargeRepON:
if not self.timerId:
self.timerId = self.startTimer(30) # update time (msec)
# add timeout
# give object a few seconds to unwrap
# self.stimer = QtCore.QTimer()
# self.stimer.singleShot(10000, self.send_killTimer)
def deleteNode(self, node):
if isinstance(node, Node):
#if (node.execType() is GPI_THREAD) and node.isProcessingEvent():
# print "Thread is in progress, cancel delete ("+node.name+")"
# return
if isMacroChildNode(node):
node.macroParent().readyForDeletion()
for n in node.getSiblingNodes():
self.nodeQueue.removeNode(n)
n.readyForDeletion()
if n.scene():
self.scene().removeItem(n)
elif node:
node.readyForDeletion()
if node.scene():
self.scene().removeItem(node)
# keep random objects from being copied to other processes
log.debug('deleteNode(): garbage collect')
gc.collect()
# try to check check for changes after a deletion
if self.inProcessingState():
self._switchSig.emit('check')
def deleteSelectedNodes(self):
'''For a list of nodes, its safer to disable all of them and remove
them from the queue directly
'''
selnodes = self.getSelectedNodes()
for node in selnodes:
node.setDeleteFlag(True)
node.setDisabledState(True)
self.nodeQueue.removeNode(node)
for node in selnodes:
self.deleteNode(node)
def deleteAllNodes(self):
'''For a list of nodes, its safer to disable all of them and remove
them from the queue directly
'''
selnodes = self.getAllNodes()
for node in selnodes:
node.setDeleteFlag(True)
node.setDisabledState(True)
self.nodeQueue.removeNode(node)
for node in selnodes:
self.deleteNode(node)
def deleteAllNodeMMAPs(self):
'''For a list of nodes, its safer to disable all of them and remove
them from the queue directly
'''
selnodes = self.getAllNodes()
for node in selnodes:
node.removeMMAPs()
def getAllMacroNodes(self):
'''Find all nodes that belong to macro-framework, then store them in a
dictionary based on macro-object-id.
'''
macros = {}
mnodes = []
for node in self.getAllNodes():
if isMacroChildNode(node):
macros[str(node.macroParent().getID())] = node.getSiblingNodes()
mnodes.append(node.macroParent())
# take encapsulated nodes if the macro is collapsed
enodes = []
for node in mnodes:
if node.isCollapsed():
enodes += node.getEncapsulatedNodes()
enodes = list(set(enodes))
return macros, enodes
def getSelectedMacroNodes(self):
'''Find all nodes that belong to macro-framework, if ANY of the child
nodes are selected then the node is selected:
-if its expanded, then only IT is selected (and any other selected
nodes)
-if its collapsed, then it AND all encapsulated nodes are selected.
'''
macros = {}
mnodes = []
for node in self.getSelectedNodes():
if isMacroChildNode(node):
macros[str(node.macroParent().getID())] = node.getSiblingNodes()
mnodes.append(node.macroParent())
# take encapsulated nodes if the macro is collapsed
enodes = []
for node in mnodes:
if node.isCollapsed():
enodes += node.getEncapsulatedNodes()
enodes = list(set(enodes))
return macros, enodes
def getAllNodes(self):
allitems = list(self.scene().items())[:] # copy in case of user interrupt
nodes = [item for item in allitems if isinstance(item, Node)]
return(nodes)
def getAllMacros(self):
'''Get the MacroNode object class handle.
'''
allitems = list(self.scene().items())[:] # copy in case of user interrupt
nodes = [item for item in allitems if isinstance(item, MacroNode)]
return(nodes)
def findNodeByNameAndLabel(self, name, lab):
# return the first occurrence of a Node with the given name and label
for node in self.getAllNodes():
if node.getNodeLabel() == lab: # most exclusive
if node.getNameFromItem() == name:
return node
def getAllPorts(self):
allitems = list(self.scene().items())[:] # copy in case of user interrupt
ports = [item for item in allitems if isinstance(item, Port)]
return(ports)
def getSelectedNodes(self):
sceneItems = list(self.scene().items())[:] # copy in case of user interrupt
sceneItems = [
i for i in sceneItems if i.isSelected() and isinstance(i, Node)]
return sceneItems
def findWidgetByID(self, nodeList, wdgid):
'''traverses all node's parmLists for the given id.
'''
wdgid = int(wdgid)
for node in nodeList:
for parm in node._nodeIF.parmList:
if parm.get_id() == wdgid:
return parm
# kill timer for items moving under charge repulsion
def send_killTimer(self):
self.killTimer(self.timerId)
self.timerId = 0
self.chargeRepON = False
def getLinearNodeHierarchy(self):
return self.getLinearNodeHierarchy_fromList(self.getAllNodes())
def getLinearNodeHierarchy_fromList(self, nodeList):
return sorted(nodeList, key=lambda y: y.getHierarchalLevel())
def viewAndSceneForcedUpdate(self):
'''All calls to this updater are to patch over a bug that
presents when a network has iterated too many times.
-The root problem needs to be found (perhaps pyqt vs. qt ownership).
-This is also required to immediately render the pause event.
'''
log.debug("viewAndSceneForcedUpdate called")
##self.updateMicroFocus()
##self.updateGeometry()
##self.repaint()
# don't bother updating if there is no gui
if Commands.noGUI():
return
self.update()
self.scene().update()
##QtGui.QApplication.processEvents() # allow gui to update
def calcNodeHierarchy(self):
# tells each node which level it is
# and returns a list based on that level
nodeList = self.getAllNodes()
# concatenate all connections (even if list is redundant)
c = []
for node in nodeList:
if len(node.getNonCyclicConnectionTuples()):
c += node.getNonCyclicConnectionTuples()
#if len(node.getConnectionTuples()):
# c += node.getConnectionTuples()
else:
# island nodes have top priority
node.resetHierarchalLevel()
node.refreshName()
sortedNodes = topsort.topsort(c)
# signal that the connection is cyclic
if sortedNodes is None:
return None
# set node hierarchy
# -each node knows its current level
cnt = 0
for node in sortedNodes:
node.setHierarchalLevel(cnt)
cnt += 1
return sortedNodes
def roundPosToGrid(self, pos):
x = int(pos[0] / self.gridRes) * self.gridRes
y = int(pos[1] / self.gridRes) * self.gridRes
return(x, y)
def keyPressEvent(self, event):
key = event.key()
modifiers = getKeyboardModifiers()
# copy/paste/delete
if key == QtCore.Qt.Key_C and modifiers == QtCore.Qt.ControlModifier:
self.copyNodesToBuffer()
elif key == QtCore.Qt.Key_V and modifiers == QtCore.Qt.ControlModifier:
# try to paste fairly close to where the nodes were copied
if self.parent._copybuffer:
s = {'sig': 'load', 'subsig': 'keypaste'}
self.addNodeRun(s)
else:
log.warn("Nothing in buffer to paste.")
elif key == QtCore.Qt.Key_Delete or key == QtCore.Qt.Key_Backspace:
#self._switchSig.emit('delete') # change state
self.deleteNodeRun('delete')
# load/save network
elif key == QtCore.Qt.Key_L and modifiers == QtCore.Qt.ControlModifier:
s = {'sig': 'load', 'subsig': 'dialog'}
self.addNodeRun(s)
#self._switchSig_info.emit(s)
elif key == QtCore.Qt.Key_S and modifiers == QtCore.Qt.ControlModifier:
self._network.saveNetworkFromFileDialog(self.serializeCanvas())
# move nodes across canvas
elif key == QtCore.Qt.Key_Up:
for node in self.getSelectedNodes():
pos = node.getPos()
x, y = self.roundPosToGrid(pos)
node.moveBy(0, y - pos[1] - 5)
elif key == QtCore.Qt.Key_Down:
for node in self.getSelectedNodes():
pos = node.getPos()
x, y = self.roundPosToGrid(pos)
node.moveBy(0, y - pos[1] + 5)
elif key == QtCore.Qt.Key_Left:
for node in self.getSelectedNodes():
pos = node.getPos()
x, y = self.roundPosToGrid(pos)
node.moveBy(x - pos[0] - 5, 0)
elif key == QtCore.Qt.Key_Right:
for node in self.getSelectedNodes():
pos = node.getPos()
x, y = self.roundPosToGrid(pos)
node.moveBy(x - pos[0] + 5, 0)
# tab nodes (in execution order)
elif key == QtCore.Qt.Key_Tab:
snodes = self.getSelectedNodes()
if len(snodes): # just skip if no nodes are selected
snode = snodes[0]
nodes = self.getLinearNodeHierarchy()
for i in range(len(nodes)):
if nodes[i] == snode:
if i < len(nodes) - 1:
self.scene().makeOnlyTheseNodesSelected(
[nodes[i + 1]])
else:
self.scene().makeOnlyTheseNodesSelected([nodes[0]])
self.scene().update()
return
else: # no nodes selected so pick one
nodes = self.getLinearNodeHierarchy()
self.scene().makeOnlyTheseNodesSelected([nodes[0]])
self.scene().update()
# raise node menu(s)
#elif key == QtCore.Qt.Key_Space:
# nodes = self.getSelectedNodes()
# if len(nodes):
# for node in nodes:
# node.menu()
elif key == QtCore.Qt.Key_Plus:
self.scaleView(1.2)
elif key == QtCore.Qt.Key_Minus:
self.scaleView(1 / 1.2)
elif key == QtCore.Qt.Key_Enter:
print("Key_Enter")
# mix up nodes
elif key == QtCore.Qt.Key_M and modifiers == QtCore.Qt.ControlModifier:
for item in list(self.scene().items()):
if isinstance(item, Node):
item.setPos(-150 + QtCore.qrand() %
300, -150 + QtCore.qrand() % 300)
# organize nodes
elif key == QtCore.Qt.Key_O and modifiers == QtCore.Qt.ControlModifier:
self.organizeSelectedNodes()
# pause
elif key == QtCore.Qt.Key_P and modifiers == QtCore.Qt.ControlModifier:
self.pauseToggle()
# select all nodes
elif key == QtCore.Qt.Key_A and modifiers == QtCore.Qt.ControlModifier:
self.scene().makeOnlyTheseNodesSelected(self.getAllNodes())
# pause -for stationary leftys
elif key == QtCore.Qt.Key_Space:
self.pauseToggle()
# charge repulsion toggle
elif key == QtCore.Qt.Key_R and int(event.modifiers()) == (QtCore.Qt.ControlModifier + QtCore.Qt.ShiftModifier):
if self.chargeRepON is True:
self.chargeRepON = False
else:
self.chargeRepON = True
self.itemMoved()
log.dialog("toggle chargeRepON:" + str(self.chargeRepON))
# reload node
elif key == QtCore.Qt.Key_R and modifiers == QtCore.Qt.ControlModifier:
self.reload_node()
# resize canvas window for podcast
elif key == QtCore.Qt.Key_W and modifiers == QtCore.Qt.ControlModifier:
log.dialog("resize window")
self.parent.resize(1024, 768)
# Test Key
elif key == QtCore.Qt.Key_T:
log.dialog("Test Key Pressed")
#print self.getAllPorts()
#print self.getAllMacroNodes()
#print self.serializeGraphData()
print((self.getAllNodes()))
print((self.getAllMacroNodes()))
# close all node windows
elif key == QtCore.Qt.Key_X and modifiers == QtCore.Qt.ControlModifier:
self.closeAllNodeMenus()
else:
super(GraphWidget, self).keyPressEvent(event)
def reload_node(self):
'''Reload, instantiate, and reconnect the selected node.
-Only allow one node.
'''
nodes = self.getSelectedNodes()
if len(nodes) == 0:
return
# pause the canvas during this process
alreadyPaused = self.inPausedState()
if not alreadyPaused:
self.pauseToggle(quiet=True)
# copy
self.copyNodesToBuffer()
# delete node
for node in nodes:
self.deleteNode(node)
# paste
if self.parent._copybuffer:
s = {'sig': 'load', 'subsig': 'reload'}
self.addNodeRun(s)
# unpause if the user hadn't already done so.
if not alreadyPaused:
self.pauseToggle()
def closeAllNodeMenus(self):
for node in self.getAllNodes():
node.closemenu()
def organizeSelectedNodes(self):
nodes = self.getSelectedNodes()
if len(nodes):
snodes = self.getLinearNodeHierarchy_fromList(nodes)
topnode = snodes[0]
x = topnode.scenePos().x()
y = topnode.scenePos().y()
self._node_anim_timeline = QtCore.QTimeLine(1000)
self._node_anim_timeline.setFrameRange(1, 100)
self._node_anims = []
for node in snodes:
self._node_anims.append(QtGui.QGraphicsItemAnimation())
self._node_anims[-1].setItem(node)
self._node_anims[-1].setTimeLine(self._node_anim_timeline)
self._node_anims[-1].setPosAt(1, QtCore.QPointF(x, y))
y += node.getNodeHeight() + 15.0
self._node_anim_timeline.start()
def chargeRepTimer(self, event):
if self.chargeRepON is False:
return
nodes = self.getAllNodes()
# don't let a single unattached node be affected by force
nodes = [item for item in nodes if len(item.edges()) > 0]
for node in nodes:
node.calculateForces()
itemsMoved = False
for node in nodes:
if node.advance():
itemsMoved = True
if not itemsMoved:
self.killTimer(self.timerId)
self.timerId = 0
def timerEvent(self, event):
# if event.timerId() == self.nodeEvent_timerId:
# print "timer: global node timer"
# always update charge rep events
self.chargeRepTimer(event)
def wheelEvent(self, event):
self.scaleView(math.pow(2.0, event.delta() / 300.0))
def drawBackground(self, painter, rect):
# Shadow.
sceneRect = self.sceneRect()
rightShadow = QtCore.QRectF(sceneRect.right(), sceneRect.top() + 5, 5,
sceneRect.height())
bottomShadow = QtCore.QRectF(sceneRect.left() + 5, sceneRect.bottom(),
sceneRect.width(), 5)
if rightShadow.intersects(rect) or rightShadow.contains(rect):
painter.fillRect(rightShadow, QtCore.Qt.darkGray)
if bottomShadow.intersects(rect) or bottomShadow.contains(rect):
painter.fillRect(bottomShadow, QtCore.Qt.darkGray)
# Fill.
gradient = QtGui.QLinearGradient(sceneRect.topLeft(),
sceneRect.bottomRight())
if self.inPausedState() and not self._pause_quiet:
gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.yellow).lighter(190))
gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.yellow).lighter(170))
else:
#gradient.setColorAt(0, QtCore.Qt.white)
#gradient.setColorAt(1, QtCore.Qt.lightGray)
gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.gray).lighter(180))
gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.gray).lighter(150))
painter.fillRect(rect.intersect(sceneRect), QtGui.QBrush(gradient))
painter.setBrush(QtCore.Qt.NoBrush)
painter.drawRect(sceneRect)
# Text.
textRect = QtCore.QRectF(sceneRect.left() + 4, sceneRect.top() + 4,
sceneRect.width() - 4, sceneRect.height() - 4)
message = "Network Canvas"
font = painter.font()
font.setBold(True)
font.setPointSize(14)
painter.setFont(font)
painter.setPen(QtCore.Qt.lightGray)
painter.drawText(textRect.translated(2, 2), message)
painter.setPen(QtCore.Qt.black)
painter.drawText(textRect, message)
# Mark.
## centered
#mark_font = QtGui.QFont(u"gill sans", 100)
#fm = QtGui.QFontMetricsF(mark_font)
#message = "PHILIPS"
#bw = fm.width(message) * 1.12
## bw is the width at 100pt font
#w = self.viewport().rect().width()
#f = (100*w)/bw/2
#mark_font = QtGui.QFont(u"gill sans", int(f))
#mark_font = QtGui.QFont(u"gill sans", 50)
#fm = QtGui.QFontMetricsF(mark_font)
#bw = fm.width(message) * 1.12
#bh = fm.height()
# centered
#textRect = QtCore.QRectF(self.mapToScene(self.viewport().rect().center()).x()-bw/2, self.mapToScene(self.viewport().rect().center()).y()-bh/2, bw, bh)
#textRect = QtCore.QRectF(self.mapToScene(self.viewport().rect().bottomRight()).x()-bw-8, self.mapToScene(self.viewport().rect().bottomRight()).y()-bh-2, bw, bh)
#mark_font.setBold(True)
#mark_font.setPointSize(14)
#painter.setFont(mark_font)
#c = QtGui.QColor(QtCore.Qt.gray).lighter(130)
#c.setAlphaF(0.5)
#painter.setPen(c)
#painter.drawText(textRect.translated(2, 2), message)
#c = QtGui.QColor(QtCore.Qt.gray).lighter(150)
#c.setAlphaF(0.5)
#painter.setPen(c)
#painter.setPen(QtCore.Qt.black)
#painter.drawText(textRect, message)
def scaleView(self, scaleFactor):
factor = self.matrix().scale(scaleFactor, scaleFactor).mapRect(
QtCore.QRectF(0, 0, 1, 1)).width()
if factor < 0.07 or factor > 100:
return
self.scale(scaleFactor, scaleFactor)
# def mouseDoubleClickEvent(self, event):
# event.accept()
# print "double-clicked canvas"
def mousePressEvent(self, event): # GRAPHICS VIEW
printMouseEvent(self, event)
modifiers = getKeyboardModifiers()
self.viewAndSceneForcedUpdate()
if self._panning:
return
QtGui.QGraphicsView.mousePressEvent(self, event)
if event.isAccepted():
return
if event.button() == QtCore.Qt.MidButton:
event.accept()
self._panning = True
# trick graphics view into thinking it has a left click for panning
leftbutton_event = QtGui.QMouseEvent(
event.type(), event.pos(), event.globalPos(),
QtCore.Qt.LeftButton, event.buttons(), modifiers)
leftbutton_event.accept()
super(GraphWidget, self).mousePressEvent(leftbutton_event)
return
# propagate to other view items (nodes)
if event.button() == QtCore.Qt.RightButton:
event.accept()
# if self.scene().itemAt(event.scenePos()):
# self.scene().unselectAllItems()
# self.scene().itemAt(event.scenePos()).setSelected(True)
pointedItem = self.itemAt(event.pos())
if not isinstance(pointedItem, InPort):
self.rightButtonMenu(event)
elif event.button() == QtCore.Qt.LeftButton:
event.accept()
# self.scene().unselectAllItems()
else:
event.ignore()
QtGui.QGraphicsView.mousePressEvent(self, event)
super(GraphWidget, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._panning or self.scene().rubberBand or self.scene().line:
self.viewAndSceneForcedUpdate()
super(GraphWidget, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event): # GRAPHICS VIEW
printMouseEvent(self, event)
modifiers = getKeyboardModifiers()
self.viewAndSceneForcedUpdate()
if self._panning:
event.accept()
leftbutton_event = QtGui.QMouseEvent(
event.type(), event.pos(), event.globalPos(),
QtCore.Qt.LeftButton, event.buttons(), modifiers)
leftbutton_event.accept()
super(GraphWidget, self).mouseReleaseEvent(leftbutton_event)
self._panning = False
return
# delete edge via input port
if event.button() == QtCore.Qt.RightButton:
pointedItem = self.itemAt(event.pos())
if isinstance(pointedItem, InPort):
edge = pointedItem.edge()
if edge:
self.scene().removeItem(edge)
# remove from ports
edge.detachSelf(tracer=True)
event.accept()
# propagate to other view items (nodes)
QtGui.QGraphicsView.mouseReleaseEvent(self, event)
if event.isAccepted():
return
if event.button() == QtCore.Qt.RightButton:
pointedItem = self.itemAt(event.pos())
if isinstance(pointedItem, InPort):
event.accept()
elif event.button() == QtCore.Qt.LeftButton:
event.accept()
# elif event.button() == QtCore.Qt.MidButton:
# event.accept()
else:
event.ignore()
QtGui.QGraphicsView.mouseReleaseEvent(self, event)
super(GraphWidget, self).mouseReleaseEvent(event)
def rightButtonMenu(self, event):
# MOUSE MENU
pointedItem = self.itemAt(event.pos())
if isinstance(pointedItem, Edge):
event.accept()
pointedItem.setSelected(True)
pointedItem.update()
menu = QtGui.QMenu(self)
deleteEdgeAction = menu.addAction("Delete")
action = menu.exec_(self.mapToGlobal(event.pos()))
if action == deleteEdgeAction:
# remove from scene
self.scene().removeItem(pointedItem)
# remove from ports
pointedItem.detachSelf()
# remove from memory
# del pointedItem
else:
pointedItem.setSelected(False)
pointedItem.update()
else:
event.accept()
# save position before any choice is made
self._event_pos = event.pos()
# main menu
menu = QtGui.QMenu(self.parent)
# search
qle = QtGui.QLineEdit()
qle.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
qle.setPlaceholderText(' Search')
qle.textChanged.connect(lambda txt: self._library.searchMenu(txt, qle, menu))
wac = QtGui.QWidgetAction(menu)
msg = 'Search for nodes and networks in the library.'
wac.hovered.connect(lambda who=msg: self.setStatusTip(who))
wac.setDefaultWidget(qle)
menu.addAction(wac)
menu.addSeparator()
# favorites at the top, then full library
#if 'Favorites' in self._library.libMenus():
# menu.addMenu(self._library.libMenus()['Favorites'])
# add a text label
#menu.addSeparator()
#menu.addAction(u'\u25BC'+u' Libraries')
menu.addSeparator()
for libmenu in self._library.libMenu():
ma = menu.addMenu(libmenu) # previously generated
msg = 'Select nodes from the \''+str(ma.text())+'\' library.'
self.connect(ma, QtCore.SIGNAL('hovered()'), lambda who=msg: self.setStatusTip(who))
pasteAct = QtGui.QAction("&Paste", self, shortcut="Ctrl+V",
statusTip="Paste node(s) from the copybuffer.")
copyAct = QtGui.QAction("Copy", self, shortcut="Ctrl+C",
statusTip="Copy node(s) to the copybuffer.")
saveAct = QtGui.QAction("Save Network", self, shortcut="Ctrl+S",
statusTip="Save network to a file.")
loadAct = QtGui.QAction("Load Network", self, shortcut="Ctrl+L",
statusTip="Load network from a file (also drag'n drop).")
layoutMenu = QtGui.QMenu('New Layout')
layoutMenu.addAction(QtGui.QAction("Vertical", self,
statusTip="Opens a vertically expanding layout window for widgets.",
triggered = lambda: self.newLayoutWindow(config=0)))
layoutMenu.addAction(QtGui.QAction("Horizontal", self,
statusTip="Opens a horizontally expanding layout window for widgets.",
triggered = lambda: self.newLayoutWindow(config=1)))
layoutMenu.addAction(QtGui.QAction("Mixed", self,
statusTip="Opens a fixed & mixed layout window for widgets.",
triggered = lambda: self.newLayoutWindow(config=2)))
layoutMenu.addAction(QtGui.QAction("Expanding", self,
statusTip="Opens an expanding mixed layout window for widgets.",
triggered = lambda: self.newLayoutWindow(config=3)))
quitAct = QtGui.QAction("Quit", self,
statusTip="Quit GPI without saving.")
clearAct = QtGui.QAction("Clear Canvas", self,
statusTip="Delete all nodes on the canvas.")
pauseAct = QtGui.QAction("Pause", self, shortcut="Ctrl+P", checkable = True, triggered=lambda: self.pauseToggle(quiet=False),
statusTip="Pause the execution queue on this canvas.")
if self.inPausedState():
pauseAct.setChecked(True)
else:
pauseAct.setChecked(False)
macroAct = QtGui.QAction("Macro Node", self, checkable = True, triggered = lambda: self.newMacroNode(event.pos()),
statusTip="Instantiate Macro Node Objects.")
if self.isMacroModule():
macroAct.setChecked(True)
else:
macroAct.setChecked(False)
# basic editor actions
menu.addSeparator()
menu.addAction(copyAct)
menu.addAction(pasteAct)
menu.addSeparator()
menu.addAction(saveAct)
menu.addAction(loadAct)
menu.addSeparator()
menu.addAction(pauseAct)
menu.addSeparator()
menu.addAction(clearAct)
menu.addAction(macroAct)
menu.addMenu(layoutMenu)
#menu.addSeparator()
#quitAction = menu.addAction(quitAct)
action = menu.exec_(self.mapToGlobal(event.pos()))
self._library.removeSearchPopup()
if action == copyAct:
self.copyNodesToBuffer()
if action == pasteAct:
# mouse-menu paste
s = {'sig': 'load', 'subsig': 'paste', 'pos': self.mapToScene(event.pos())}
self.addNodeRun(s)
if action == clearAct: # DELETE
#self._switchSig.emit('deleteAll') # change state
reply = QtGui.QMessageBox.question(self, 'Message',
"Delete all modules on this canvas?", QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No, QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
self.deleteNodeRun('deleteAll')
if action == loadAct:
s = {'sig': 'load', 'subsig': 'dialog',
'pos': self.mapToScene(event.pos())}
self.addNodeRun(s)
#self._switchSig_info.emit(s)
if action == saveAct:
self._network.saveNetworkFromFileDialog(self.serializeCanvas())
if action == quitAct:
pass
# pausing might make quitting more graceful
#self._switchSig.emit('pause')
#self.closeGraph(event)
#QtGui.qApp.quit()
self.parent.statusBar().clearMessage()
def setStatusTip(self, msg):
self.parent.statusBar().showMessage(msg)
def newMacroNode(self, pos):
log.debug("drop new macro")
log.debug(str(pos))
if isinstance(pos, QtCore.QPointF):
pos = QtCore.QPoint(pos.x(), pos.y())
newnode = MacroNode(self, QtCore.QPointF(self.mapToScene(pos)))
#self.scene().addItem(newnode)
return newnode
def showMacroTools(self):
'''Show the src, sink, and macro layout window.
'''
self.macroModuleToggle()
if self.isMacroModule():
log.debug("show macro tools")
else:
reply = QtGui.QMessageBox.question(self, 'Message',
"Turn off macro settings for this canvas?\n\nAny src/sink " + \
"connections and macro-layouts will be removed.",
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
log.debug("hide macro tools")
else:
log.debug("cancel hide")
self.setMacroModule(True)
def isMacroModule(self):
return self._macroModule
def setMacroModule(self, val):
self._macroModule = val
def macroModuleToggle(self):
if self._macroModule:
self._macroModule = False
else:
self._macroModule = True
def closeEvent(self, event):
self.closeGraphNoDialog()
event.accept()
def closeGraphWithDialog(self):
reply = QtGui.QMessageBox.question(self, 'Message',
"Close canvas without saving?", QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No, QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
self.closeGraphNoDialog()
return True
return False
def closeGraphNoDialog(self):
'''This is the close procedure for a canvas.
'''
self._switchSig.emit('pause')
self.deleteNodeRun('deleteAll')
for lm in self._layoutwindowList:
if lm: # some may have already been closed
lm.close()
def pauseToggle(self, quiet=False):
'''toggle the pause state for attached buttons.
'''
if self.inPausedState():
self._switchSig.emit('unpause')
else:
if quiet:
self._switchSig_info.emit({'sig':'pause', 'subsig':'quiet'})
else:
self._switchSig.emit('pause')
def copyNodesToBuffer(self):
self.parent._copybuffer = self.serializeGraphData(selectedOnly=True)
# NETWORK SERIALIZATION
def deserializeGraphData(self, graph_settings, layoutSettings=[], pos=None, offset=False, randoffset=False, reloadnode=False):
log.info("num nodes: " + str(len(graph_settings['nodes'])))
# determine network center randomly to avoid overlap
rx = 0
ry = 0
if offset:
# this places modules at a slight offset to their orig-pos.
# used for keystroke copy/paste
# -keep original position and add this offset
radius = 5.0 # pts
rx += radius * 6.0
ry += radius * 6.0
if randoffset:
# if a network is loaded by menu more than once, this small
# perturbation makes it easy to distinguish between multiple
# instances
radius = 5.0 # pts
rx += random.random() * radius
ry += random.random() * radius
if pos:
# if the position is supplied then the graph should be
# instantiated relative to it.
graph_settings = self.subtractAvgPosFromSettings(graph_settings)
rx += pos.x()
ry += pos.y()
# temporarily buffer nodes in case this network is going to be merged
buf = []
macro_buf = []
skipped_mods = []
new_nodes = []
# for reloading nodes use the existing nodes on the canvas so that
# the IDs match for connecting edges.
if reloadnode:
buf += self.getAllNodes()
# place all nodes on the canvas
for s in graph_settings['nodes']:
log.debug("add node: " + str(s['name']))
# try to import the node module by name
if s['name'] == '__GPIMacroNode__':
continue
# instantiate node
cpos = QtCore.QPoint(s['pos'][0] + rx, s['pos'][1] + ry)
# first always try to get the node by library
node = self.newNode_byKey(s['key'], cpos)
if node is None:
log.warn('Failed to find node \''+stw(s['name']) + '\' by scope.')
# get a list of widget and port names together
wdg_port_names = []
for parm in s['widget_settings']['parms']:
wdg_port_names.append(parm['name'])
for port in s['ports']:
wdg_port_names.append(port['porttitle'])
# find from libarary and instantiate on the canvas
node = self.newNode_byClosestMatch(s['name'], wdg_port_names, cpos)
# final failure to resolve node
if node is None:
log.error('Node \''+stw(s['name']) + '\' failed to load, skipping.')
skipped_mods.append(str(s['name']))
continue
new_nodes.append(node)
node.setDisabledState(True) # put in disabled state
node.setID(s['id'])
buf.append(node)
# set other node attributes
if 'walltime' in s:
try: # deprecate this try statement
node.appendWallTime(float(s['walltime']))
except:
log.error(stw(node.getModuleName()) + ' has no walltime but walltime was saved as NoneType, skipping...')
node.loadNodeIFSettings(s['widget_settings'])
# place all macro nodes on the canvas and load settings
# -done after node instantiation so that widgets can be copied over
log.debug("Load MacroNodes:")
log.debug(str(graph_settings['macroNodes']))
for s in graph_settings['macroNodes']:
mnode = MacroNode(self, QtCore.QPointF(rx, ry))
mnode.loadSettings(s, buf, (rx, ry))
macro_buf.append(mnode)
for node in mnode.getNodes():
buf.append(node)
# once all nodes have been placed,
# start making connections
for s in graph_settings['nodes']:
# remake the connections
for port in s['ports']:
for c in port['connections']:
# make a new edge given src and dest
# get the nodes
src = self.getNodeByID(buf, c['src']['nodeID'])
dst = self.getNodeByID(buf, c['dest']['nodeID'])
if src and dst:
# get the ports
#outport = src.getPortByNumOrTitle(c['src']['portName'])
#inport = dst.getPortByNumOrTitle(c['dest']['portName'])
outport = src.getOutPort(c['src']['portName'])
inport = dst.getInPort(c['dest']['portName'])
# each connection is stored twice (memory of each node)
# only use one for each pair
try:
log.debug("inport title: "+inport.portTitle)
if len(inport.edges()) > 0:
log.debug("Inport occupied," \
+ " connection dropped.")
else:
# make the connection
newEdge = Edge(outport, inport)
self.scene().addItem(newEdge)
except:
log.warn("Duplicate or connection" \
+ " not found. Skip connection.")
else:
log.warn("Src and Dst not" \
+ " accurately saved. Skip connection.")
self.scene().unselectAllItems()
# load layouts before the ids get reset.
for lw in layoutSettings:
self.newLayoutWindowFromSettings(lw, buf)
# reset node IDs, and widget IDs
for node in buf:
node.setID()
for parm in node.getParmList():
parm.set_id()
# reset node IDs, and widget IDs select only newly loaded network items
if reloadnode:
# for reloading nodes
for node in new_nodes:
node.setSelected(True)
node.setEventStatus({GPI_INIT_EVENT: None})
node.displayReloaded()
else:
# for importing networks
for node in buf:
node.setSelected(True)
node.setEventStatus({GPI_INIT_EVENT: None})
# reset macro IDs
for node in macro_buf:
if node.shouldCollapse():
node.setCollapse(True)
node.resetIDs()
self.calcNodeHierarchy()
# put nodes back in to idle
for node in buf:
node.setDisabledState(False)
QtGui.QApplication.processEvents() # allow gui to update
if len(skipped_mods):
log.error("Failed to load the following modules: ")
for name in skipped_mods:
log.error("\t" + name)
if not reloadnode:
try:
# get top most node position-wise
# and make sure its visible
topnode = buf[0]
for node in buf:
if node.pos().y() < topnode.pos().y():
topnode = node
self.ensureVisible(topnode)
except:
log.warn("Can\'t determine top node, skipping.")
def getNodeByID(self, buf, nid):
for item in buf:
if isinstance(item, Node):
if item.getID() == nid:
return item
def calcAvgPosFromSettings(self, graph_settings):
cnt = 0.
ax = 0.
ay = 0.
# macro nodes
for mnode in graph_settings['macroNodes']:
node = mnode['src_settings']
ax += node['pos'][0]
ay += node['pos'][1]
cnt += 1.
node = mnode['sink_settings']
ax += node['pos'][0]
ay += node['pos'][1]
cnt += 1.
node = mnode['face_settings']
ax += node['pos'][0]
ay += node['pos'][1]
cnt += 1.
# normal nodes
for node in graph_settings['nodes']:
ax += node['pos'][0]
ay += node['pos'][1]
cnt += 1.
ax = ax / cnt
ay = ay / cnt
return(ax, ay)
def subtractAvgPosFromSettings(self, graph_settings):
ax, ay = self.calcAvgPosFromSettings(graph_settings)
newgraphsettings = copy.deepcopy(graph_settings)
#newgraphsettings = graph_settings
# macro nodes
for mnode in newgraphsettings['macroNodes']:
node = mnode['src_settings']
node['pos'][0] -= ax
node['pos'][1] -= ay
node = mnode['sink_settings']
node['pos'][0] -= ax
node['pos'][1] -= ay
node = mnode['face_settings']
node['pos'][0] -= ax
node['pos'][1] -= ay
for node in newgraphsettings['nodes']:
node['pos'][0] -= ax
node['pos'][1] -= ay
return(newgraphsettings)
def serializeGraphData(self, selectedOnly=False, minusAvgPos=False):
# Handles only nodes and macro-nodes. The 'selectedOnly' option is
# for copy/paste operation.
graph_settings = {}
graph_settings['nodes'] = []
graph_settings['macroNodes'] = []
# serialize all nodes and macro nodes
if selectedOnly:
nodes = self.getSelectedNodes()
macroNodes, enodes = self.getSelectedMacroNodes()
nodes = list(set(nodes + enodes))
else:
nodes = self.getAllNodes()
macroNodes, enodes = self.getAllMacroNodes()
nodes = list(set(nodes + enodes))
for node in nodes:
graph_settings['nodes'].append(copy.deepcopy(node.getSettings()))
for nid, nodes in list(macroNodes.items()):
graph_settings['macroNodes'].append(nodes[0].macroParent().getSettings())
if minusAvgPos and (len(graph_settings['nodes']) + len(graph_settings['macroNodes'])):
graph_settings = self.subtractAvgPosFromSettings(graph_settings)
return graph_settings
def serializeCanvas(self):
network = {}
network['nodes'] = self.serializeGraphData(minusAvgPos=True)
network['layouts'] = self.serializeLayoutWindows()
network['WALLTIME'] = str(self.walltime()) # sec
network['TOTAL_PMEM'] = str(self.totalPortMem()) # bytes
return network
def deserializeCanvas(self, network, pos):
# convert the loaded file data into the format required by GPI objects
# and instantiate the network on the canvas.
nodes = network['nodes']
layouts = network['layouts']
if nodes:
log.info("load nodes.")
else:
log.error("network description contains no node information!!!")
return
if layouts:
log.info("network has layouts.")
self.deserializeGraphData(nodes, layoutSettings=layouts, pos=pos)
else:
self.deserializeGraphData(nodes, pos=pos)