# 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.
from gpi import QtCore, QtGui
# gpi
from .port import InPort, OutPort, Port
from .edge import Edge
from .macroNode import PortEdge
from .node import Node
from .defines import GPI_PORT_EVENT
from .defines import getKeyboardModifiers, printMouseEvent
from .logger import manager
# start logger for this module
log = manager.getLogger(__name__)
[docs]class CanvasScene(QtGui.QGraphicsScene):
'''Supports the main canvas widget by drawing shapes for displaying
interactions between elements on the canvas (e.g. selecting nodes). '''
def __init__(self, parent=None):
super(CanvasScene, self).__init__(parent)
self.graph = parent
self.line = None
self.rubberBand = None
self.origin = QtCore.QPointF()
# during a port connection this is used to hold
# a list of matching port types for highlighting
self.portMatches = []
def startLineDraw(self, event):
# highlight all ports that match
startItems = self.items(event.scenePos())
if len(startItems):
if isinstance(startItems[0], OutPort):
if QtCore.Qt.AltModifier == getKeyboardModifiers():
startItems[0].toggleMemSaver()
return
self.portMatches = startItems[0].findMatchingInPorts()
for port in self.portMatches:
port.scaleUp()
self.graph.viewAndSceneForcedUpdate()
if isinstance(startItems[0], InPort):
self.portMatches = startItems[0].findMatchingOutPorts()
for port in self.portMatches:
port.scaleUp()
self.graph.viewAndSceneForcedUpdate()
self.line = QtGui.QGraphicsLineItem(
QtCore.QLineF(event.scenePos(), event.scenePos()))
fade = QtGui.QColor(QtCore.Qt.red)
fade.setAlpha(150)
self.line.setPen(QtGui.QPen(fade, 2))
self.line.setZValue(10)
self.addItem(self.line)
def midLineDraw(self, event):
newLine = QtCore.QLineF(self.line.line().p1(), event.scenePos())
self.line.setLine(newLine)
# attach source or sink connection
#if QtCore.Qt.AltModifier == getKeyboardModifiers():
# self.line.setPen(QtGui.QPen(QtCore.Qt.blue, 2))
# self.dumpCursorStack()
# QtGui.QApplication.setOverrideCursor(
# QtGui.QCursor(QtCore.Qt.CrossCursor))
# return
#else:
# self.line.setPen(QtGui.QPen(QtCore.Qt.red, 2))
startItems = self.items(event.scenePos())
if len(startItems) and startItems[0] == self.line:
startItems.pop(0)
self.dumpCursorStack()
QtGui.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.ForbiddenCursor))
if len(startItems):
if isinstance(startItems[0], Port):
self.dumpCursorStack()
QtGui.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.CrossCursor))
def endLineDraw(self, event):
self.dumpCursorStack()
QtGui.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.OpenHandCursor))
QtGui.QApplication.restoreOverrideCursor()
startItems = self.items(self.line.line().p1())
# if there are no valid start items then mark for removal
if len(startItems) and startItems[0] == self.line:
startItems.pop(0)
# reset any hovering events
if len(startItems):
if isinstance(startItems[0], Port):
startItems[0].hoverLeaveEvent(event)
# if there are no valid end items then mark for removal
endItems = self.items(self.line.line().p2())
if len(endItems) and endItems[0] == self.line:
endItems.pop(0)
# remove temporary drag line
self.removeItem(self.line)
self.line = None
# if there are valid start and end items then continue
if len(startItems) and len(endItems):
# connect ports
if isinstance(startItems[0], Port) and isinstance(endItems[0], Port) \
and startItems[0] != endItems[0] and type(startItems[0]) != type(endItems[0]):
# find which is which
if isinstance(startItems[0], InPort):
inport = startItems[0]
outport = endItems[0]
else:
inport = endItems[0]
outport = startItems[0]
# check if the ports belong to the same node
if inport.node == outport.node:
log.warn("CanvasScene: inport and outport belong to the same node.")
super(CanvasScene, self).mouseReleaseEvent(event)
return
# check the number of connections on the inport
# if one exists then skip
if len(inport.edges()) > 0:
log.warn("CanvasScene: inport occupied, connection dropped")
super(CanvasScene, self).mouseReleaseEvent(event)
return
# This, currently, is the only way edges know
# which port is actually a source or dest.
# NOTE: Network description files will have to enforce this.
newEdge = Edge(outport, inport)
self.addItem(newEdge)
# if its cyclic then don't allow the connection
# -at the same time, calculate the hierarchy
nodeHierarchy = inport.getNode().graph.calcNodeHierarchy()
if nodeHierarchy is None:
self.removeItem(newEdge)
newEdge.detachSelf()
# del newEdge
log.warn("CanvasScene: cyclic, connection dropped")
else:
# CONNECTION ADDED
# Since node hierarchy is recalculated, also
# take the time to flag nodes for processing
# 1) check for matching spec type
if not (inport.checkUpstreamPortType()):
self.removeItem(newEdge)
newEdge.detachSelf(update=False)
# del newEdge
log.warn("CanvasScene: data type mismatch, connection dropped")
else:
# 2) set the downstream node's pending_event
inport.getNode().setEventStatus({GPI_PORT_EVENT: inport.portTitle})
# trigger a force recalculation
inport.getNode().graph.itemMoved()
# trigger name update
inport.getNode().refreshName()
outport.getNode().refreshName()
# trigger event queue, if its idle
inport.getNode().graph._switchSig.emit('check')
if len(self.portMatches):
for port in self.portMatches:
port.resetScale()
self.portMatches = []
inport.edges()[0].adjust()
for edge in outport.edges():
edge.adjust()
# remove edge by drawing a line across it
elif isinstance(startItems[0], Edge) and isinstance(startItems[0], Edge) and startItems[0] == endItems[0]:
# remove from scene
self.removeItem(startItems[0])
# remove from ports
startItems[0].detachSelf()
# remove from memory
# del startItems[0]
# reset the port size changes during port connect.
if len(self.portMatches):
for port in self.portMatches:
port.resetScale()
self.portMatches = []
self.line = None
def dumpCursorStack(self):
while QtGui.QApplication.overrideCursor():
QtGui.QApplication.restoreOverrideCursor()
def mousePressEvent(self, event): # CANVAS SCENE
printMouseEvent(self, event)
modifiers = getKeyboardModifiers()
# allow graphics view panning
if self.graph._panning:
event.ignore()
return
# if its not a port, then don't draw a line
modmidbutton_event = (event.button() == QtCore.Qt.LeftButton
and modifiers == QtCore.Qt.AltModifier)
if ((event.button() == QtCore.Qt.LeftButton) or (event.button() == QtCore.Qt.MidButton) or modmidbutton_event) \
and isinstance(self.itemAt(event.scenePos()), Port):
event.accept()
self.startLineDraw(event)
# rubber band select
# elif ((event.button() == QtCore.Qt.MidButton) or modmidbutton_event):
elif ((event.button() == QtCore.Qt.LeftButton) \
and not isinstance(self.itemAt(event.scenePos()), Node) \
and not isinstance(self.itemAt(event.scenePos()), PortEdge)):
event.accept()
self.unselectAllItems() # reset select before making another
self.origin = event.scenePos()
self.rubberBand = QtGui.QGraphicsRectItem(
QtCore.QRectF(self.origin, QtCore.QSizeF()))
self.rubberBand.setPen(QtGui.QPen(
QtCore.Qt.gray, 0, QtCore.Qt.SolidLine))
self.rubberBand.setBrush(QtGui.QBrush(QtCore.Qt.lightGray))
self.rubberBand.setZValue(0)
self.addItem(self.rubberBand)
else:
QtGui.QApplication.restoreOverrideCursor()
event.ignore()
super(CanvasScene, self).mousePressEvent(event)
def mouseMoveEvent(self, event): # CANVAS SCENE
# allow graphics view panning
if self.graph._panning:
event.ignore()
return
if self.line:
event.accept()
self.midLineDraw(event)
elif self.rubberBand:
event.accept()
newRect = QtCore.QRectF(self.origin, event.scenePos())
self.rubberBand.setRect(newRect)
else:
event.ignore()
self.dumpCursorStack()
super(CanvasScene, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event): # CANVAS SCENE
# allow graphics view panning
if self.graph._panning:
event.ignore()
return
printMouseEvent(self, event)
if self.line:
event.accept()
self.endLineDraw(event)
elif self.rubberBand:
event.accept()
selarea = self.rubberBand.rect()
self.setSelectedNodes(selarea)
self.removeItem(self.rubberBand)
self.rubberBand = None
else:
event.ignore()
self.line = None
super(CanvasScene, self).mouseReleaseEvent(event)
def unselectAllItems(self):
# TODO: add Z level changes here too
for item in list(self.items()):
if item.isSelected():
item.setSelected(False)
def setSelectedItems(self, box):
self.unselectAllItems()
for item in self.items(box):
item.setSelected(True)
def setSelectedNodes(self, box):
self.unselectAllItems()
for item in self.items(box):
if isinstance(item, Node):
item.setSelected(True)
def makeOnlyTheseNodesSelected(self, nodes):
# need to distinguish between nodes and other items
self.unselectAllItems()
for node in nodes:
node.setSelected(True)
node.update()