view python/apps/diagrameditor.py @ 87:367006d423ae

Changed selection handles
author windel
date Fri, 23 Nov 2012 18:27:29 +0100
parents 317a73256bf0
children f3fe557be5ed
line wrap: on
line source

#!/usr/bin/python

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys, json, base64

"""
 Author: Windel Bouwman
 Year: 2012
 Description: This script implements a diagram editor.
 run with python 3.x as:
  $ python [thisfile.py]
"""
saveicon = b'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\nveAfetNmUUsM6bsAAAAASUVORK5CYII=\n'

loadicon = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACsElEQVR42u2Xf0hTURTHv++ZczlD\n6SeUREVRiRT9gFWM1rbAwr+iYAathPqjMosIQQqJQAhLLDI0CMwflRa1vyQst0oNLKxhCGWR9UfB\nwFCRaqC2vc6d7+1tr/7YnuwW5IEv95y79875vHvP7t4E/GUTpgGmAaL8vaRzpDlx3iuRXpEOkz5N\nFWDhrHR8bL+LtGXLKUqRJcoKyeUk1Q8Fges3gLIKeGlm+1QBHA4rPJ7WxG4eHqLlWoJRcvfJaPHY\nV1LPbwB5Dnja7if+BFUVQEdX7OqETdL48vD+C/DODze5e9iUCmAjgDt6FzJ+m/gG5GyD9OEz1lPY\nqwLQFrQ1JR+Amc0JPO0O941XBbAQQB0nABcBvNACbCGAWk4ABwngpRbATACXEksU7rsJGlnzUSZB\nkEdAbW9lXgnJtx0jAJ8WYCMBnE8MIESFx7+rRQRRLiaPESANmL2EAF6jgaLSCIA9Bx53sZokJpEY\nm4zFzP85BgTn0mUZUL9y7CPpz8CCvAJdA0DrE6D6NnxsLp9UaV2KVfW7Jy8QWcGUyUJhP0piFJA0\nmw6iAn19EAgAmdkICpRw5EEdsjabgXQDYo9gRZENVxVeIJO8Gjqs7y2wxoJ+IXUGxkcHkDrTqC+R\nXrt5D3AVoYU9yJs+L1bnruQLUFJO+34NpxlAc2MVCly7+ALkHQAedSKfAZwq3o/KK2X8ikvUT4ss\nkPyDWMwAtm5ai47uZn4Ag8PAAgvoxxzzGUCGMQ1DI89gMBr4ADymtwHHIXSSa1UOIt/zBqwz5/IB\nqG4Bjl9EDblFCkDN5ZM4csLJB+DoBaDWDTp3cVUBcDntaLx1hg/AzlKgvQc7yH2oAGSbjOg/WwjT\nvMzkFvdT65U34UdgDCtYGP1avoFUSMpK8gKw7q8n9bLgn/pj8n8C/ALihrxpNKi7hQAAAABJRU5E\nrkJggg==\n'

newicon = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADSUlEQVR42r2XO0xaURjHLzYmdXRi\nYXBwdDScJl3djQNLw0TSAR88FR9JU6P4QKMCaTqQdGgXsQQMQXFwcGXhCCG+4iNEo6mDMZL45kK/\n76THUO+9Qi/efskJcm4O3+/+v8f51Al/LBaLmTs6Or43gAkq7eHhoXh2dmZva2v7WusZHf9jZGTE\nPz4+blfjuFwuC6IoCsViUbi8vCwDhM1oNH75V4AAANjUAJRKJeYc183NjXB1dVU+Pj7+CIp+qxlg\neHg44PV6VQHg2z8+PjKAu7s7oampSQAVxL29PUtnZ+eP/waA6/r6WtDr9UyJ09NTcXd319LV1aUI\n8QQwNDQUmJiYqBsA5BcMBgPLC4Q4OTkRt7e3LSaTSRbiCWBwcDAwOTmpOgfQOVSBcHFxIbS0tLB9\nDgH5IGYyGYvZbJZAvAoAOsL4IwTEngGxH9fp2MJnhUJBTKfT5t7e3rAsgMfjYQB4oB4V+OJAuI+A\naGtra6nz8/P3o6OjJQnAwMBAYGpqSjUAh8Aw8JLE3OBqIMTGxgbNZrPE5/MVFQFUe6+A4M5xoWMe\nIg4ASksB+vv761aAG3eKMFwBBFhfX6ebm5sEyl0eAHOgjqvgRSBUA3KAghHouFIAt9vNFNACgIci\nmUxSKEcyNjYmBXC5XK8Wgudvz8OAAJgDigD1lGE155UAsiFwOp2vUgVKzisBZJMQAdR2wlqco62s\nrNBcLkfgzpECOByOuhXg5cc733NLJBIMQLYP2O12BqB0uJrjyk8li8fjdGtri4AfeQAMAUpXayJW\ne2MlANlWbLPZGAB2LPagCoQapZaXl+nOzg6ZmZmRB8CBhANoYTB5U5iQyOzsrDwAjmR4hWpl0WiU\nwpxI5ubmpAAwKLAQ4HWqlUUiEbq/v0/m5+flATAEWiqwtLREDw4OyMLCgjyA1iEIh8P08PCQ+P1+\nKUBPTw8D0DIEi4uL9OjoiASDQSlAd3c3A7i/v9cUAKsgFApJAaxWa2B6etp2e3v71yGlen++X60v\n4EwAAOlUKvUO+oEUoLW19UNfX9/nxsbGhsoOxy+Wl75X2+drdXX1J/zX9AkmI+lU3N7e/hYSxAAT\n0Rs+FdX69rXsA1y5ubn5Vz6fL1Q++w30VO4/0/9IewAAAABJRU5ErkJggg==\n'

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 = 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 []

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

def indent(lines):
   return ['   ' + line for line in lines]

class Connection(QGraphicsPathItem):
   """ Implementation of 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 = QPen(Qt.blue, 2, cap=Qt.RoundCap)
      self.setPen(pen)
      self.arrowhead = 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(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 = 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 BlockItem]
         scene.removeItem(litem)
      pts = [pos1] + vias + [pos2]
      self.arrowhead.setPos(pos2)
      self.arrowhead.setRotation(90)
      p = buildPath(pts)
      pps = QPainterPathStroker()
      pps.setWidth(3)
      p = pps.createStroke(p).simplified()
      self.setPath(p)
      """ Create a shape outline using the path stroker """
      s = super(Connection, self).shape()
      pps = QPainterPathStroker()
      pps.setWidth(10)
      self.myshape = pps.createStroke(s).simplified()

class ParameterDialog(QDialog):
   def __init__(self, block, parent = None):
      super(ParameterDialog, self).__init__(parent)
      self.block = block
      self.button = QPushButton('Ok', self)
      self.nameEdit = QLineEdit(self.block.name)
      self.codeEdit = QTextEdit(self)
      self.codeEdit.setPlainText(self.block.code)
      l = QFormLayout(self)
      l.addRow('Name:', self.nameEdit)
      l.addRow('Code:', self.codeEdit)
      l.addWidget(self.button)
      self.button.clicked.connect(self.OK)
   def OK(self):
      self.block.name = self.nameEdit.text()
      self.block.code = self.codeEdit.toPlainText()
      self.close()

class PortItem(QGraphicsPathItem):
   """ Represents a port to a subsystem """
   def __init__(self, name, block):
      super(PortItem, self).__init__(block)
      self.textItem = QGraphicsTextItem(self)
      self.connection = None
      self.block = block
      self.setCursor(QCursor(Qt.CrossCursor))
      self.setPen(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(QGraphicsEllipseItem):
   """ A handle that can be moved by the mouse """
   def __init__(self, parent=None):
      dx = 13.0
      super(Handle, self).__init__(QRectF(-0.5*dx,-0.5*dx,dx,dx), parent)
      self.posChangeCallbacks = []
      self.setBrush(QBrush(Qt.white))
      self.setFlags(self.ItemSendsScenePositionChanges | self.ItemIsMovable)
      self.setVisible(False)
      self.setCursor(QCursor(Qt.SizeFDiagCursor))
   def mouseMoveEvent(self, event):
      """ Move function without moving the other selected elements """
      p = self.mapToParent(event.pos())
      self.setPos(p)
   def itemChange(self, change, value):
      if change == self.ItemPositionChange:
         for cb in self.posChangeCallbacks:
            res = cb(value)
            if res: value = res
         return value
      return super(Handle, self).itemChange(change, value)

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

class BlockItem(QGraphicsRectItem):
   """ Represents a block in the diagram """
   def __init__(self, name='Untitled', parent=None):
      super(BlockItem, self).__init__(parent)
      self.subModel = DiagramScene()
      self.subModel.containingBlock = self
      # Properties of the rectangle:
      self.setPen(QPen(Qt.blue, 2))
      self.setBrush(QBrush(Qt.lightGray))
      self.setFlags(self.ItemIsSelectable | self.ItemIsMovable | self.ItemSendsScenePositionChanges)
      self.setCursor(QCursor(Qt.PointingHandCursor))
      self.label = QGraphicsTextItem(name, self)
      self.name = name
      self.code = ''
      # Create corner for resize:
      button = QPushButton('+in')
      button.clicked.connect(self.newInputPort)
      self.buttonItemAddInput = QGraphicsProxyWidget(self)
      self.buttonItemAddInput.setWidget(button)
      self.buttonItemAddInput.setVisible(False)
      button = QPushButton('+out')
      button.clicked.connect(self.newOutputPort)
      self.buttonItemAddOutput = QGraphicsProxyWidget(self)
      self.buttonItemAddOutput.setWidget(button)
      self.buttonItemAddOutput.setVisible(False)
      # Inputs and outputs of the block:
      self.inputs = []
      self.outputs = []
   def editParameters(self):
      pd = ParameterDialog(self, self.window())
      pd.exec_()
   def mouseDoubleClickEvent(self, event):
      #self.editParameters()
      scene = self.scene()
      if scene:
         for view in scene.views():
            view.diagram = self.subModel
            view.zoomAll()
   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.update({'name': self.name, 'code': self.code})
      d['inputs'] = [inp.Dict for inp in self.inputs]
      d['outputs'] = [outp.Dict for outp in self.outputs]
      d['submodel'] = self.subModel.Dict
      return d
   def setDict(self, d):
      self.name = d['name']
      self.code = d['code']
      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))
      self.subModel.Dict = d['submodel']
   Dict = property(getDict, setDict)
   def gencode(self):
      c = ['def {0}():'.format(self.name)]
      if self.code:
         c += indent(self.code.split('\n'))
      else:
         c += indent(['pass'])
      return c
   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)
      return super(BlockItem, self).itemChange(change, value)
   def myDelete(self):
      for p in self.inputs + self.outputs:
         if p.connection: p.connection.myDelete()
      self.scene().removeItem(self)
   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):
      h = 20 if h < 20 else h
      w = 40 if w < 40 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 EditorGraphicsView(QGraphicsView):
   def __init__(self, parent=None):
      QGraphicsView.__init__(self, parent)
      self.setDragMode(QGraphicsView.RubberBandDrag)
      self.delShort = QShortcut(QKeySequence.Delete, self)
      self._model = None
      self.treeView = QTreeView()
   def setDiagram(self, d):
      self.setScene(d)
      self.delShort.activated.connect(d.deleteItems)
   def getModel(self): return self._model
   def setModel(self, m):
      self._model = m
      if m:
         self.treeView.setModel(m)
         self.diagram = m.rootDiagram
         self.model.modelReset.connect(self.treeView.expandAll)
   model = property(getModel, setModel)
   diagram = property(lambda s: s.scene(), setDiagram)
   def save(self):
      if self.model:
         if not self.model.filename:
            self.model.filename = QFileDialog.getSaveFileName(self)
         if self.model.filename:
            with open(self.model.filename, 'w') as f:
               f.write(json.dumps(self.model.Dict))
   def load(self):
      filename = QFileDialog.getOpenFileName(self)
      if filename:
         self.model = loadModel(filename)
   def newModel(self):
      self.model = ModelHierarchyModel()
   def goUp(self):
      if hasattr(self.diagram, 'containingBlock'):
         self.diagram = self.diagram.containingBlock.scene()
         self.zoomAll()
   def showCode(self):
      if self.model:
         c = self.model.gencode()
         c = '\n'.join(c)
         d = QDialog()
         l = QFormLayout(d)
         codeview = QTextEdit()
         codeview.setPlainText(c)
         l.addRow('code', codeview)
         runButton = QPushButton('Run')
         outputview = QTextEdit()
         l.addRow('Output', outputview)
         l.addWidget(runButton)
         def print2(txt):
            txt2 = outputview.toPlainText()
            outputview.setPlainText(txt2 + '\n' + txt)
         def runIt():
            outputview.clear()
            globs = {'print': print2}
            exec(codeview.toPlainText(), globs)
         runButton.clicked.connect(runIt)
         d.exec_()
   def zoomAll(self):
      """ zoom to fit all items """
      rect = self.diagram.itemsBoundingRect()
      self.fitInView(rect, Qt.KeepAspectRatio)
   def wheelEvent(self, event):
      pos = event.pos()
      posbefore = self.mapToScene(pos)
      degrees = event.delta() / 8.0
      sx = (100.0 + degrees) / 100.0
      self.scale(sx, sx)
      event.accept()
   def dragEnterEvent(self, event):
      if event.mimeData().hasFormat('component/name'): event.accept()
   def dragMoveEvent(self, event):
      if event.mimeData().hasFormat('component/name'): event.accept()
   def dropEvent(self, event):
      if event.mimeData().hasFormat('component/name'):
         name = bytes(event.mimeData().data('component/name')).decode()
         pos = self.mapToScene(event.pos())
         self.scene().addNewBlock(pos, name)
         # TODO: do this in a cleaner way:
         self.model.modelReset.emit()

class LibraryModel(QStandardItemModel):
   mimeTypes = lambda self: ['component/name']
   def mimeData(self, idxs):
      mimedata = QMimeData()
      for idx in idxs:
         if idx.isValid():
            txt = self.data(idx, Qt.DisplayRole)
            mimedata.setData('component/name', txt)
      return mimedata

class ModelHierarchyModel(QAbstractItemModel):
   def __init__(self):
      super(ModelHierarchyModel, self).__init__()
      self.rootDiagram = DiagramScene()
      self.filename = None
   def setDict(self, d):
      self.rootDiagram.Dict = d
      self.modelReset.emit()
   Dict = property(lambda s: s.rootDiagram.Dict, setDict)
   def gencode(self):
      c = ['def topLevel():']
      c += indent(self.rootDiagram.gencode())
      c.append('print("Running model")')
      c.append('topLevel()')
      c.append('print("Done")')
      return c
   def index(self, row, column, parent=None):
      if parent.isValid():
         parent = parent.internalPointer().subModel
      else:
         parent = self.rootDiagram
      blocks = sorted(parent.blocks, key=lambda b:  b.name)
      block = blocks[row]
      # Store the index to retrieve it later in the parent function.
      # TODO: solve this in a better way.
      block.index = self.createIndex(row, column, block)
      return block.index
   def parent(self, index):
      if index.isValid():
         block = index.internalPointer()
         if block.scene() == self.rootDiagram:
            return QModelIndex()
         else:
            outerBlock = block.scene().containingBlock
            return outerBlock.index
      print('parent: No valid index')
   def data(self, index, role):
      if index.isValid() and role == Qt.DisplayRole:
         b = index.internalPointer()
         return b.name
   def headerData(self, section, orientation, role):
      if orientation == Qt.Horizontal and role == Qt.DisplayRole:
         return "Element"
   def rowCount(self, parent):
      if parent.column() > 0: return 0
      if parent.isValid():
         parent = parent.internalPointer().subModel
      else:
         parent = self.rootDiagram
      return len(parent.blocks)
   def columnCount(self, parent):
      return 1

class DiagramScene(QGraphicsScene):
   def __init__(self):
      super(DiagramScene, self).__init__()
      self.startedConnection = None
      self.selectionHandles = [ResizeSelectionHandle(i) for i in range(8)]
      for h in self.selectionHandles:
         self.addItem(h)
         h.setVisible(False)
      self.selectionChanged.connect(self.handleSelectionChanged)
   def repositionAndShowHandles(self):
         r = self.selectionRect
         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 handleSelectionChanged(self):
      [h.setVisible(False) for h in self.selectionHandles]
      items = self.selectedItems()
      items = [i for i in items if type(i) is BlockItem]
      if items:
         r = QRectF()
         for i in items:
            r = r.united(i.boundingRect().translated(i.scenePos()))
         self.selectionRect = r
         self.repositionAndShowHandles()
   def sizerMoveEvent(self, handle, pos):
      if handle.position == Position.TOP_LEFT: self.selectionRect.setTopLeft(pos)
      elif handle.position == Position.TOP: self.selectionRect.setTop(pos.y())
      elif handle.position == Position.TOP_RIGHT: self.selectionRect.setTopRight(pos)
      elif handle.position == Position.RIGHT: self.selectionRect.setRight(pos.x())
      elif handle.position == Position.BOTTOM_RIGHT: self.selectionRect.setBottomRight(pos)
      elif handle.position == Position.BOTTOM: self.selectionRect.setBottom(pos.y())
      elif handle.position == Position.BOTTOM_LEFT: self.selectionRect.setBottomLeft(pos)
      elif handle.position == Position.LEFT: self.selectionRect.setLeft(pos.x())
      else:
         print('invalid position')
      self.repositionAndShowHandles()
      items = self.selectedItems()
      items = [i for i in items if type(i) is BlockItem]
      if items:
         item = items[0]
         # TODO resize more items!
         item.setCenterAndSize(self.selectionRect.center(), self.selectionRect.size())

   blocks = property(lambda sel: [i for i in sel.items() if type(i) is BlockItem])
   connections = property(lambda sel: [i for i in sel.items() if type(i) is Connection])
   def setDict(self, d):
      for block in d['blocks']:
         b = BlockItem()
         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 addNewBlock(self, pos, name):
      blocknames = [item.name for item in self.blocks]
      b1 = BlockItem(uniqify(name, blocknames))
      b1.setPos(pos)
      self.addItem(b1)
   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()

class LibraryWidget(QListView):
   def __init__(self):
      super(LibraryWidget, self).__init__(None)
      self.libraryModel = LibraryModel(self)
      self.libraryModel.setColumnCount(1)
      # Create an icon with an icon:
      pixmap = QPixmap(60, 60)
      pixmap.fill()
      painter = QPainter(pixmap)
      painter.fillRect(10, 10, 40, 40, Qt.blue)
      painter.setBrush(Qt.yellow)
      painter.drawEllipse(20, 20, 20, 20)
      painter.end()
      # Fill library:
      for name in ['Block', 'Uber unit', 'Device']:
         self.libraryModel.appendRow(QStandardItem(QIcon(pixmap), name))
      self.setModel(self.libraryModel)
      self.setViewMode(self.IconMode)
      self.setDragDropMode(self.DragOnly)

def warning(txt):
   QMessageBox.warning(None, "Warning", txt)

def loadModel(filename):
   try:
      m = ModelHierarchyModel()
      with open(filename, 'r') as f: data = f.read()
      m.filename = filename
      m.Dict = json.loads(data)
      return m
   except KeyError:
      warning('Corrupt model: {0}'.format(filename))
   except ValueError:
      warning('Corrupt model: {0}'.format(filename))
   except FileNotFoundError:
      warning('File [{0}] not found'.format(filename))

class Main(QMainWindow):
   def __init__(self):
      super(Main, self).__init__(None)
      self.editor = EditorGraphicsView()
      self.setCentralWidget(self.editor)
      self.setWindowTitle("Diagram editor")
      def buildIcon(b64):
         icon = base64.decodestring(b64)
         pm = QPixmap()
         pm.loadFromData(icon)
         return QIcon(pm)
      toolbar = self.addToolBar('Tools')
      toolbar.setObjectName('Tools')
      def act(name, shortcut, callback, icon=None):
         a = QAction(icon, name, self) if icon else QAction(name, self)
         a.setShortcuts(shortcut)
         a.triggered.connect(callback)
         toolbar.addAction(a)
      act('New', QKeySequence.New, self.editor.newModel, buildIcon(newicon))
      act('Save', QKeySequence.Save, self.editor.save, buildIcon(saveicon))
      act('Load', QKeySequence.Open, self.editor.load, buildIcon(loadicon))
      act('Full screen', QKeySequence("F11"), self.toggleFullScreen)
      act('Fit in view', QKeySequence("F8"), self.editor.zoomAll)
      act('Go up', QKeySequence(Qt.Key_Up), self.editor.goUp)
      act('Model code', QKeySequence("F7"), self.editor.showCode)
      def addDock(name, widget):
         dock = QDockWidget(name, self)
         dock.setObjectName(name)
         dock.setWidget(widget)
         self.addDockWidget(Qt.LeftDockWidgetArea, dock)
      addDock('Library', LibraryWidget())
      addDock('Model tree', self.editor.treeView)
      self.settings = QSettings('windelsoft', 'diagrameditor')
      self.loadSettings()
   def toggleFullScreen(self):
      self.setWindowState(self.windowState() ^ Qt.WindowFullScreen)
      self.editor.zoomAll()
   def loadSettings(self):
      if self.settings.contains('mainwindowstate'):
         self.restoreState(self.settings.value('mainwindowstate'))
      if self.settings.contains('mainwindowgeometry'):
         self.restoreGeometry(self.settings.value('mainwindowgeometry'))
      if self.settings.contains('openedmodel'):
         modelfile = self.settings.value('openedmodel')
         self.editor.model = loadModel(modelfile)
   def closeEvent(self, ev):
      self.settings.setValue('mainwindowstate', self.saveState())
      self.settings.setValue('mainwindowgeometry', self.saveGeometry())
      if self.editor.model and self.editor.model.filename:
         self.settings.setValue('openedmodel', self.editor.model.filename)
         # TODO: ask for save of opened files
      else:
         self.settings.remove('openedmodel')
      ev.accept()

if __name__ == '__main__':
   if sys.version_info.major != 3:
      print('Please use python 3.x')
      sys.exit(1)
   app = QApplication(sys.argv)
   main = Main()
   main.show()
   app.exec_()