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)