Mercurial > lcfOS
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_()