view python/other/diagramitems.py @ 390:b77f3290ac79

Added diagram editor tests
author Windel Bouwman
date Fri, 16 May 2014 10:12:16 +0200
parents 534b94b40aa8
children bb4289c84907
line wrap: on
line source

"""
 Contains all blocks that can be used to build models.
"""

import sys
import json
import base64
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'ide'))

from qtwrapper import QtGui, QtCore, QtWidgets, pyqtSignal, get_icon
from qtwrapper import abspath, Qt


def uniqify(name, names):
    newname, i = name, 1
    while newname in names: newname, i = name + str(i), i + 1
    return newname


def enum(**enums):
    return type('Enum', (), enums)


Position = enum(TOP=0, TOP_RIGHT=1, RIGHT=2, BOTTOM_RIGHT=3, BOTTOM=4, BOTTOM_LEFT=5, LEFT=6, TOP_LEFT=7)


def buildPath(pts):
    path = QtGui.QPainterPath(pts[0])
    for pt in pts[1:]: path.lineTo(pt)
    return path

def equalSpace(n, l, offset=15):
    if n == 1:
        return [l / 2]
    elif n > 1:
        return [offset + (l - offset*2)/(n - 1)*i for i in range(n)]
    return []


class Connection(QtWidgets.QGraphicsPathItem):
   """ A connection between blocks """
   def __init__(self, fromPort=None, toPort=None):
      super(Connection, self).__init__()
      self.pos2 = self.fromPort = self.toPort = None
      self.setFlags(self.ItemIsSelectable | self.ItemClipsToShape)
      pen = QtGui.QPen(Qt.blue, 2, cap=Qt.RoundCap)
      self.setPen(pen)
      self.arrowhead = QtGui.QGraphicsPathItem(self)
      self.arrowhead.setPath(buildPath([QPointF(0.0, 0.0), QPointF(-6.0, 10.0), QPointF(6.0, 10.0), QPointF(0.0, 0.0)]))
      self.arrowhead.setPen(pen)
      self.arrowhead.setBrush(QtGui.QBrush(pen.color()))
      self.vias = []
      self.setFromPort(fromPort)
      self.setToPort(toPort)
   def getDict(self):
      d = {}
      d['fromBlock'] = self.fromPort.block.name
      d['fromPort'] = self.fromPort.name
      d['toBlock'] = self.toPort.block.name
      d['toPort'] = self.toPort.name
      return d
   Dict = property(getDict)
   def myDelete(self):
      scene = self.scene()
      if scene:
         self.setFromPort(None)
         self.setToPort(None)
         scene.removeItem(self)
   def setFromPort(self, fromPort):
      if self.fromPort:
         self.fromPort.posCallbacks.remove(self.setBeginPos)
         self.fromPort.connection = None
      self.fromPort = fromPort
      if self.fromPort:
         self.fromPort.connection = self
         self.updateLineStukken()
         self.fromPort.posCallbacks.append(self.setBeginPos)
   def setToPort(self, toPort):
      if self.toPort:
         self.toPort.posCallbacks.remove(self.setEndPos)
         self.toPort.connection = None
      self.toPort = toPort
      if self.toPort:
         self.setEndPos(toPort.scenePos())
         self.toPort.connection = self
         self.toPort.posCallbacks.append(self.setEndPos)
   def getPos1(self):
      if self.fromPort:
         return self.fromPort.scenePos()
   def setBeginPos(self, pos1): self.updateLineStukken()
   def setEndPos(self, endpos):
      self.pos2 = endpos
      self.updateLineStukken()
   def itemChange(self, change, value):
      if change == self.ItemSelectedHasChanged:
         for via in self.vias:
            via.setVisible(value)
      return super(Connection, self).itemChange(change, value)
   def shape(self): return self.myshape
   def updateLineStukken(self):
      """
         This algorithm determines the optimal routing of all signals.
         TODO: implement nice automatic line router
      """
      pos1 = self.getPos1()
      pos2 = self.pos2
      if pos1 is None or pos2 is None:
         return
      scene = self.scene()
      vias = [pos1 + QPointF(20, 0)] + self.vias + [pos2 + QPointF(-20, 0)]
      if scene:
         litem = QtGui.QGraphicsLineItem()
         litem.setFlags(self.ItemIsSelectable)
         scene.addItem(litem)
         for p1, p2 in zip(vias[:-1], vias[1:]):
            line = QLineF(p1, p2)
            litem.setLine(line)
            citems = scene.collidingItems(litem)
            citems = [i for i in citems if type(i) is Block]
         scene.removeItem(litem)
      pts = [pos1] + vias + [pos2]
      self.arrowhead.setPos(pos2)
      self.arrowhead.setRotation(90)
      p = buildPath(pts)
      self.setPath(p)
      """ Create a shape outline using the path stroker """
      s = super(Connection, self).shape()
      pps = QtGui.QPainterPathStroker()
      pps.setWidth(10)
      self.myshape = pps.createStroke(s).simplified()

class PortItem(QtWidgets.QGraphicsPathItem):
   """ Represents a port to a subsystem """
   def __init__(self, name, block):
      super(PortItem, self).__init__(block)
      self.textItem = QtGui.QGraphicsTextItem(self)
      self.connection = None
      self.block = block
      self.setCursor(QtGui.QCursor(Qt.CrossCursor))
      self.setPen(QtGui.QPen(Qt.blue, 2, cap=Qt.RoundCap))
      self.name = name
      self.posCallbacks = []
      self.setFlag(self.ItemSendsScenePositionChanges, True)
   def getName(self): return self.textItem.toPlainText()
   def setName(self, name):
      self.textItem.setPlainText(name)
      rect = self.textItem.boundingRect()
      lw, lh = rect.width(), rect.height()
      lx = 3 if type(self) is InputPort else -3 - lw
      self.textItem.setPos(lx, -lh / 2)
   name = property(getName, setName)
   def getDict(self): 
      return {'name': self.name}
   Dict = property(getDict)
   def itemChange(self, change, value):
      if change == self.ItemScenePositionHasChanged:
         for cb in self.posCallbacks: cb(value)
         return value
      return super(PortItem, self).itemChange(change, value)

class OutputPort(PortItem):
   def __init__(self, name, block, d=10.0):
      super(OutputPort, self).__init__(name, block)
      self.setPath(buildPath([QPointF(0.0, -d), QPointF(d, 0), QPointF(0.0, d)]))
   def mousePressEvent(self, event):
      self.scene().startConnection(self)

class InputPort(PortItem):
   def __init__(self, name, block, d=10.0):
      super(InputPort, self).__init__(name, block)
      self.setPath(buildPath([QPointF(-d, -d), QPointF(0, 0), QPointF(-d, d)]))

class Handle(QtWidgets.QGraphicsEllipseItem):
   """ A handle that can be moved by the mouse """
   def __init__(self, dx=10.0, parent=None):
      super(Handle, self).__init__(QtCore.QRectF(-0.5*dx,-0.5*dx,dx,dx), parent)
      self.setBrush(QtGui.QBrush(Qt.white))
      self.setFlags(self.ItemIsMovable)
      self.setZValue(1)
      self.setVisible(False)
      self.setCursor(QtGui.QCursor(Qt.SizeFDiagCursor))
   def mouseMoveEvent(self, event):
      """ Move function without moving the other selected elements """
      p = self.mapToParent(event.pos())
      self.setPos(p)

class ResizeSelectionHandle(Handle):
   def __init__(self, position, block):
      super(ResizeSelectionHandle, self).__init__(dx=12, parent=block)
      self.position = position
      self.block = block
      if position in [Position.TOP_LEFT, Position.BOTTOM_RIGHT]:
         self.setCursor(QtGui.QCursor(Qt.SizeFDiagCursor))
      elif position in [Position.TOP_RIGHT, Position.BOTTOM_LEFT]:
         self.setCursor(QCursor(Qt.SizeBDiagCursor))
      elif position in [Position.TOP, Position.BOTTOM]:
         self.setCursor(QCursor(Qt.SizeVerCursor))
      elif position in [Position.LEFT, Position.RIGHT]:
         self.setCursor(QtGui.QCursor(Qt.SizeHorCursor))
   def mouseMoveEvent(self, event):
      self.block.sizerMoveEvent(self, event.scenePos())

class Block(QtWidgets.QGraphicsRectItem):
   """ Represents a block in the diagram. """
   def __init__(self, name='Untitled', parent=None):
      super(Block, self).__init__(parent)
      self.selectionHandles = [ResizeSelectionHandle(i, self) for i in range(8)]
      # Properties of the rectangle:
      self.setPen(QtGui.QPen(Qt.blue, 2))
      self.setBrush(QtGui.QBrush(Qt.lightGray))
      self.setFlags(self.ItemIsSelectable | self.ItemIsMovable | self.ItemSendsScenePositionChanges)
      self.setCursor(QtGui.QCursor(Qt.PointingHandCursor))
      self.setAcceptHoverEvents(True)
      self.label = QtWidgets.QGraphicsTextItem(name, self)
      self.name = name
      # Create corner for resize:
      button = QtWidgets.QPushButton('+in')
      button.clicked.connect(self.newInputPort)
      self.buttonItemAddInput = QtWidgets.QGraphicsProxyWidget(self)
      self.buttonItemAddInput.setWidget(button)
      self.buttonItemAddInput.setVisible(False)
      button = QtWidgets.QPushButton('+out')
      button.clicked.connect(self.newOutputPort)
      self.buttonItemAddOutput = QtWidgets.QGraphicsProxyWidget(self)
      self.buttonItemAddOutput.setWidget(button)
      self.buttonItemAddOutput.setVisible(False)
      # Inputs and outputs of the block:
      self.inputs = []
      self.outputs = []
      self.changeSize(2,2)
   def editParameters(self):
      pd = ParameterDialog(self, self.window())
      pd.exec_()
   def newInputPort(self):
      names = [i.name for i in self.inputs + self.outputs]
      self.addInput(InputPort(uniqify('in', names), self))
   def newOutputPort(self):
      names = [i.name for i in self.inputs + self.outputs]
      self.addOutput(OutputPort(uniqify('out', names), self))
   def setName(self, name): self.label.setPlainText(name)
   def getName(self): return self.label.toPlainText()
   name = property(getName, setName)
   def getDict(self):
      d = {'x': self.scenePos().x(), 'y': self.scenePos().y()}
      rect = self.rect()
      d.update({'width': rect.width(), 'height': rect.height()})
      d['name'] = self.name
      d['inputs'] = [inp.Dict for inp in self.inputs]
      d['outputs'] = [outp.Dict for outp in self.outputs]
      return d
   def setDict(self, d):
      self.name = d['name']
      self.setPos(d['x'], d['y'])
      self.changeSize(d['width'], d['height'])
      for inp in d['inputs']:
         self.addInput(InputPort(inp['name'], self))
      for outp in d['outputs']:
         self.addOutput(OutputPort(outp['name'], self))
   Dict = property(getDict, setDict)
   def addInput(self, i):
      self.inputs.append(i)
      self.updateSize()
   def addOutput(self, o):
      self.outputs.append(o)
      self.updateSize()
   def contextMenuEvent(self, event):
      menu = QMenu()
      pa = menu.addAction('Parameters')
      pa.triggered.connect(self.editParameters)
      menu.exec_(event.screenPos())
   def itemChange(self, change, value):
      if change == self.ItemSelectedHasChanged:
         for child in [self.buttonItemAddInput, self.buttonItemAddOutput]:
            child.setVisible(value)
         if value:
            self.repositionAndShowHandles()
         else:
            [h.setVisible(False) for h in self.selectionHandles]

      return super(Block, self).itemChange(change, value)
   def hoverEnterEvent(self, event):
      if not self.isSelected():
         self.repositionAndShowHandles()
      super(Block, self).hoverEnterEvent(event)
   def hoverLeaveEvent(self, event):
      if not self.isSelected():
         [h.setVisible(False) for h in self.selectionHandles]
      super(Block, self).hoverLeaveEvent(event)
   def myDelete(self):
      for p in self.inputs + self.outputs:
         if p.connection: p.connection.myDelete()
      self.scene().removeItem(self)
   def repositionAndShowHandles(self):
         r = self.rect()
         self.selectionHandles[Position.TOP_LEFT].setPos(r.topLeft())
         self.selectionHandles[Position.TOP].setPos(r.center().x(), r.top())
         self.selectionHandles[Position.TOP_RIGHT].setPos(r.topRight())
         self.selectionHandles[Position.RIGHT].setPos(r.right(), r.center().y())
         self.selectionHandles[Position.BOTTOM_RIGHT].setPos(r.bottomRight())
         self.selectionHandles[Position.BOTTOM].setPos(r.center().x(), r.bottom())
         self.selectionHandles[Position.BOTTOM_LEFT].setPos(r.bottomLeft())
         self.selectionHandles[Position.LEFT].setPos(r.left(), r.center().y())
         for h in self.selectionHandles:
            h.setVisible(True)
   def sizerMoveEvent(self, handle, pos):
      r = self.rect().translated(self.pos())
      if handle.position == Position.TOP_LEFT: r.setTopLeft(pos)
      elif handle.position == Position.TOP: r.setTop(pos.y())
      elif handle.position == Position.TOP_RIGHT: r.setTopRight(pos)
      elif handle.position == Position.RIGHT: r.setRight(pos.x())
      elif handle.position == Position.BOTTOM_RIGHT: r.setBottomRight(pos)
      elif handle.position == Position.BOTTOM: r.setBottom(pos.y())
      elif handle.position == Position.BOTTOM_LEFT: r.setBottomLeft(pos)
      elif handle.position == Position.LEFT: r.setLeft(pos.x())
      else:
         print('invalid position')
      self.setCenterAndSize(r.center(), r.size())
      self.repositionAndShowHandles()
   def updateSize(self):
      rect = self.rect()
      h, w = rect.height(), rect.width()
      self.buttonItemAddInput.setPos(0, h + 4)
      self.buttonItemAddOutput.setPos(w+10, h+4)
      for inp, y in zip(self.inputs, equalSpace(len(self.inputs), h)):
         inp.setPos(0.0, y)
      for outp, y in zip(self.outputs, equalSpace(len(self.outputs), h)):
         outp.setPos(w, y)
   def setCenterAndSize(self, center, size):
      self.changeSize(size.width(), size.height())
      p = QPointF(size.width(), size.height())
      self.setPos(center - p / 2)
   def changeSize(self, w, h):
      minw = 150
      minh = 50
      h = minh if h < minh else h
      w = minw if w < minw else w
      self.setRect(0.0, 0.0, w, h)
      rect = self.label.boundingRect()
      self.label.setPos((w - rect.width()) / 2, (h - rect.height()) / 2)
      self.updateSize()


class CodeBlock(Block):
    def __init__(self, name='Untitled', parent=None):
        super(CodeBlock, self).__init__(name, parent)
        self.code = ''

    def setDict(self, d):
        super(CodeBlock, self).setDict(d)
        self.code = d['code']

    def getDict(self):
        d = super(CodeBlock, self).getDict()
        d['code'] = self.code
        return d

    def gencode(self):
      c = ['def {0}():'.format(self.name)]
      if self.code:
         c += indent(self.code.split('\n'))
      else:
         c += indent(['pass'])
      return c


class DiagramBlock(Block):
    def __init__(self, name='Untitled', parent=None):
      super(DiagramBlock, self).__init__(name, parent)
      self.subModel = DiagramScene()
      self.subModel.containingBlock = self

    def setDict(self, d):
        self.subModel.Dict = d['submodel']

    def mouseDoubleClickEvent(self, event):
      # descent into child diagram
      #self.editParameters()
      print('descent')
      scene = self.scene()
      if scene:
         for view in scene.views():
            view.diagram = self.subModel
            view.zoomAll()


class DiagramScene(QtWidgets.QGraphicsScene):
   """ A diagram scene consisting of blocks and connections """
   structureChanged = pyqtSignal()
   def __init__(self):
      super(DiagramScene, self).__init__()
      self.startedConnection = None

   blocks = property(lambda sel: [i for i in sel.items() if isinstance(i, Block)])
   connections = property(lambda sel: [i for i in sel.items() if type(i) is Connection])
   def addItem(self, item):
      super(DiagramScene, self).addItem(item)
      if isinstance(item, Block):
         self.structureChanged.emit()
   def removeItem(self, item):
      super(DiagramScene, self).removeItem(item)
      if isinstance(item, Block):
         self.structureChanged.emit()
   def setDict(self, d):
      for block in d['blocks']:
         b = Block()
         self.addItem(b)
         b.Dict = block
      for con in d['connections']:
         fromPort = self.findPort(con['fromBlock'], con['fromPort'])
         toPort = self.findPort(con['toBlock'], con['toPort'])
         self.addItem(Connection(fromPort, toPort))
   def getDict(self):
      return {'blocks': [b.Dict for b in self.blocks], 'connections': [c.Dict for c in self.connections]}
   Dict = property(getDict, setDict)
   def gencode(self):
      c = []
      for b in self.blocks:
         c += b.gencode()
      for b in self.blocks:
         c.append('{0}()'.format(b.name))
      return c
   def findPort(self, blockname, portname):
      block = self.findBlock(blockname)
      if block:
         for port in block.inputs + block.outputs:
            if port.name == portname: return port
   def findBlock(self, blockname):
      for block in self.blocks:
         if block.name == blockname: return block
   def uniqify(self, name):
      blocknames = [item.name for item in self.blocks]
      return uniqify(name, blocknames)
   def mouseMoveEvent(self, event):
      if self.startedConnection:
         pos = event.scenePos()
         self.startedConnection.setEndPos(pos)
      super(DiagramScene, self).mouseMoveEvent(event)
   def mouseReleaseEvent(self, event):
      if self.startedConnection:
         for item in self.items(event.scenePos()):
            if type(item) is InputPort and item.connection == None:
               self.startedConnection.setToPort(item)
               self.startedConnection = None
               return
         self.startedConnection.myDelete()
         self.startedConnection = None
      super(DiagramScene, self).mouseReleaseEvent(event)
   def startConnection(self, port):
      self.startedConnection = Connection(port, None)
      pos = port.scenePos()
      self.startedConnection.setEndPos(pos)
      self.addItem(self.startedConnection)
   def deleteItems(self):
      for item in list(self.selectedItems()): item.myDelete()