Mercurial > lcfOS
view python/apps/diagrameditor.py @ 64:4a27c28c7d0f
File movage
author | windel |
---|---|
date | Sun, 07 Oct 2012 17:13:47 +0200 |
parents | 32078200cdd6 |
children | b01311fb3be7 |
line wrap: on
line source
#!/usr/bin/python from PyQt4.QtGui import * from PyQt4.QtCore import * import sys import xml.dom.minidom as md import xml """ Author: Windel Bouwman Year: 2012 Description: This script implements a diagram editor. run with python 3.x as: $ python [thisfile.py] """ class ArrowHead(QGraphicsPathItem): def __init__(self, parent): super(ArrowHead, self).__init__(parent) arrowPath = QPainterPath(QPointF(0.0, 0.0)) arrowPath.lineTo(-6.0, 10.0) arrowPath.lineTo(6.0, 10.0) arrowPath.lineTo(0.0, 0.0) self.setPath(arrowPath) pen = QPen(Qt.blue, 2) self.setPen(pen) self.setBrush(QBrush(pen.color())) self.myshape = QPainterPath() class Connection(QGraphicsPathItem): """ Implementation of a connection between blocks """ def __init__(self, fromPort, toPort): super(Connection, self).__init__() self.pos1 = None self.pos2 = None self.fromPort = None self.toPort = None self.setFlag(self.ItemIsSelectable, True) self.setFlag(self.ItemClipsToShape, True) self.pen = QPen(Qt.blue, 2) self.pen.setCapStyle(Qt.RoundCap) self.setPen(self.pen) self.arrowhead = ArrowHead(self) self.vias = [] self.setFromPort(fromPort) self.setToPort(toPort) def mouseDoubleClickEvent(self, event): pos = event.scenePos() pts = [self.pos1] + [v.pos() for v in self.vias] + [self.pos2] idx = 0 tidx = 0 for p1, p2 in zip(pts[0:-1], pts[1:]): l1 = QLineF(p1, p2) l2 = QLineF(p1, pos) l3 = QLineF(pos, p2) d = l2.length() + l3.length() - l1.length() if d < 5: tidx = idx idx += 1 self.addHandle(pos, tidx) def addHandle(self, pos, idx=None): hi = HandleItem(self) if idx: self.vias.insert(idx, hi) else: self.vias.append(hi) def callback(p): self.updateLineStukken() return p hi.posChangeCallbacks.append(callback) hi.setPos(pos) self.updateLineStukken() 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.setBeginPos(fromPort.scenePos()) 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 releasePorts(self): self.setFromPort(None) self.setToPort(None) def setBeginPos(self, pos1): self.pos1 = 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 """ if self.pos1 is None or self.pos2 is None: return pts = [self.pos1] + [v.pos() for v in self.vias] + [self.pos2] self.arrowhead.setPos(self.pos2) if pts[-1].x() < pts[-2].x(): self.arrowhead.setRotation(-90) else: self.arrowhead.setRotation(90) path = QPainterPath(pts[0]) for pt in pts[1:]: path.lineTo(pt) self.setPath(path) """ 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) l = QGridLayout(self) l.addWidget(QLabel('Name:', self), 0, 0) self.nameEdit = QLineEdit(self.block.name) l.addWidget(self.nameEdit, 0, 1) l.addWidget(QLabel('Code:', self), 1, 0) self.codeEdit = QTextEdit(self) self.codeEdit.setPlainText(self.block.code) l.addWidget(self.codeEdit, 1, 1) l.addWidget(self.button, 2, 0, 1, 2) self.button.clicked.connect(self.OK) def OK(self): self.block.setName(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, direction): super(PortItem, self).__init__(block) self.connection = None path = QPainterPath() d = 10.0 if direction == 'input': path.moveTo(-d, -d) path.lineTo(0.0, 0.0) path.lineTo(-d, d) else: path.moveTo(0.0, -d) path.lineTo(d, 0.0) path.lineTo(0.0, d) self.setPath(path) self.direction = direction self.block = block self.setCursor(QCursor(Qt.CrossCursor)) pen = QPen(Qt.blue, 2) pen.setCapStyle(Qt.RoundCap) self.setPen(pen) self.name = name self.textItem = QGraphicsTextItem(name, self) self.setName(name) self.posCallbacks = [] self.setFlag(self.ItemSendsScenePositionChanges, True) def setName(self, name): self.name = name self.textItem.setPlainText(name) rect = self.textItem.boundingRect() lw, lh = rect.width(), rect.height() if self.direction == 'input': lx = 3 else: lx = -3 - lw self.textItem.setPos(lx, -lh / 2) 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) def mousePressEvent(self, event): if self.direction == 'output': self.scene().startConnection(self) class OutputPort(PortItem): # TODO: create a subclass OR make a member porttype pass # Block part: class HandleItem(QGraphicsEllipseItem): """ A handle that can be moved by the mouse """ def __init__(self, parent=None): dx = 13.0 super(HandleItem, self).__init__(QRectF(-0.5*dx,-0.5*dx,dx,dx), parent) self.posChangeCallbacks = [] self.setBrush(QBrush(Qt.white)) self.setFlag(self.ItemSendsScenePositionChanges, True) self.setFlag(self.ItemIsMovable, True) 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 mySetPos(self, p): # TODO: use this instead of itemChange? 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 # Call superclass method: return super(HandleItem, self).itemChange(change, value) def uniqify(name, names): newname = name i = 1 while newname in names: newname = name + str(i) i += 1 return newname class BlockItem(QGraphicsRectItem): """ Represents a block in the diagram Has an x and y and width and height width and height can only be adjusted with a tip in the lower right corner. - in and output ports - parameters - description """ def __init__(self, name='Untitled', parent=None): super(BlockItem, self).__init__(parent) # Properties of the rectangle: self.setPen(QPen(Qt.blue, 2)) self.setBrush(QBrush(Qt.lightGray)) self.setFlags(self.ItemIsSelectable | self.ItemIsMovable) self.setFlag(self.ItemSendsScenePositionChanges, True) self.setCursor(QCursor(Qt.PointingHandCursor)) self.label = QGraphicsTextItem(name, self) self.name = name self.code = '' # Create corner for resize: self.sizer = HandleItem(self) self.sizer.posChangeCallbacks.append(self.changeSize) # Connect the callback 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 = [] # Update size: self.sizer.mySetPos(QPointF(60, 40)) # This is a better resize function def editParameters(self): pd = ParameterDialog(self, self.window()) pd.exec_() def mouseDoubleClickEvent(self, event): self.editParameters() def newInputPort(self): names = [i.name for i in self.inputs + self.outputs] self.addInput(PortItem(uniqify('in', names), self, 'input')) def newOutputPort(self): names = [i.name for i in self.inputs + self.outputs] self.addOutput(PortItem(uniqify('out', names), self, 'output')) def setName(self, name): self.name = name self.label.setPlainText(name) 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: self.sizer.setVisible(value) self.buttonItemAddInput.setVisible(value) self.buttonItemAddOutput.setVisible(value) return super(BlockItem, self).itemChange(change, value) 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) if len(self.inputs) == 1: self.inputs[0].setPos(0.0, h / 2) elif len(self.inputs) > 1: y = 15 dy = (h - 30) / (len(self.inputs) - 1) for inp in self.inputs: inp.setPos(0.0, y) y += dy if len(self.outputs) == 1: self.outputs[0].setPos(w, h / 2) elif len(self.outputs) > 1: y = 15 dy = (h - 30) / (len(self.outputs) - 1) for outp in self.outputs: outp.setPos(w, y) y += dy def changeSize(self, p): """ Resize block function """ w, h = p.x(), p.y() # Limit the block size: if h < 20: h = 20 if w < 40: w = 40 self.setRect(0.0, 0.0, w, h) # center label: rect = self.label.boundingRect() lw, lh = rect.width(), rect.height() lx = (w - lw) / 2 ly = (h - lh) / 2 self.label.setPos(lx, ly) # Update port positions: self.updateSize() return QPointF(w, h) class EditorGraphicsView(QGraphicsView): def __init__(self, scene, parent=None): QGraphicsView.__init__(self, scene, parent) self.setDragMode(QGraphicsView.RubberBandDrag) 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) class LibraryModel(QStandardItemModel): def __init__(self, parent=None): QStandardItemModel.__init__(self, parent) def mimeTypes(self): return ['component/name'] def mimeData(self, idxs): mimedata = QMimeData() for idx in idxs: if idx.isValid(): txt = self.data(idx, Qt.DisplayRole) # python 3 mimedata.setData('component/name', txt) return mimedata class DiagramScene(QGraphicsScene): """ Save and load and deletion of item""" def __init__(self, parent=None): super(DiagramScene, self).__init__(parent) self.startedConnection = None def saveDiagram(self, filename): items = self.items() blocks = [item for item in items if type(item) is BlockItem] connections = [item for item in items if type(item) is Connection] doc = md.Document() modelElement = doc.createElement('system') doc.appendChild(modelElement) for block in blocks: blockElement = doc.createElement("block") x, y = block.scenePos().x(), block.scenePos().y() rect = block.rect() w, h = rect.width(), rect.height() blockElement.setAttribute("name", block.name) blockElement.setAttribute("x", str(int(x))) blockElement.setAttribute("y", str(int(y))) blockElement.setAttribute("width", str(int(w))) blockElement.setAttribute("height", str(int(h))) codeNode = doc.createCDATASection(block.code) codeElement = doc.createElement('code') codeElement.appendChild(codeNode) blockElement.appendChild(codeElement) for inp in block.inputs: portElement = doc.createElement("input") portElement.setAttribute("name", inp.name) blockElement.appendChild(portElement) for outp in block.outputs: portElement = doc.createElement("output") portElement.setAttribute("name", outp.name) blockElement.appendChild(portElement) modelElement.appendChild(blockElement) for connection in connections: connectionElement = doc.createElement("connection") fromPort = connection.fromPort.name toPort = connection.toPort.name fromBlock = connection.fromPort.block.name toBlock = connection.toPort.block.name connectionElement.setAttribute("fromBlock", fromBlock) connectionElement.setAttribute("fromPort", fromPort) connectionElement.setAttribute("toBlock", toBlock) connectionElement.setAttribute("toPort", toPort) for via in connection.vias: viaElement = doc.createElement('via') viaElement.setAttribute('x', str(int(via.x()))) viaElement.setAttribute('y', str(int(via.y()))) connectionElement.appendChild(viaElement) modelElement.appendChild(connectionElement) with open(filename, 'w') as f: f.write(doc.toprettyxml()) def loadDiagram(self, filename): try: doc = md.parse(filename) except IOError as e: print('{0} not found'.format(filename)) return except xml.parsers.expat.ExpatError as e: print('{0}'.format(e)) return sysElements = doc.getElementsByTagName('system') blockElements = doc.getElementsByTagName('block') for sysElement in sysElements: blockElements = sysElement.getElementsByTagName('block') for blockElement in blockElements: x = float(blockElement.getAttribute('x')) y = float(blockElement.getAttribute('y')) w = float(blockElement.getAttribute('width')) h = float(blockElement.getAttribute('height')) name = blockElement.getAttribute('name') block = BlockItem(name) self.addItem(block) block.setPos(x, y) block.sizer.setPos(w, h) codeElements = blockElement.getElementsByTagName('code') if codeElements: cn = codeElements[0].childNodes cdatas = [cd for cd in cn if type(cd) is md.CDATASection] if len(cdatas) > 0: block.code = cdatas[0].data # Load ports: portElements = blockElement.getElementsByTagName('input') for portElement in portElements: name = portElement.getAttribute('name') inp = PortItem(name, block, 'input') block.addInput(inp) portElements = blockElement.getElementsByTagName('output') for portElement in portElements: name = portElement.getAttribute('name') outp = PortItem(name, block, 'output') block.addOutput(outp) connectionElements = sysElement.getElementsByTagName('connection') for connectionElement in connectionElements: fromBlock = connectionElement.getAttribute('fromBlock') fromPort = connectionElement.getAttribute('fromPort') toBlock = connectionElement.getAttribute('toBlock') toPort = connectionElement.getAttribute('toPort') viaElements = connectionElement.getElementsByTagName('via') fromPort = self.findPort(fromBlock, fromPort) toPort = self.findPort(toBlock, toPort) connection = Connection(fromPort, toPort) for viaElement in viaElements: x = int(viaElement.getAttribute('x')) y = int(viaElement.getAttribute('y')) connection.addHandle(QPointF(x, y)) self.addItem(connection) def findPort(self, blockname, portname): items = self.items() blocks = [item for item in items if type(item) is BlockItem] for block in [b for b in blocks if b.name == blockname]: for port in block.inputs + block.outputs: if port.name == portname: return port def addNewBlock(self, pos, name): blocknames = [item.name for item in self.items() if type(item) is BlockItem] 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: items = self.items(event.scenePos()) for item in items: if type(item) is PortItem: self.startedConnection.setToPort(item) self.startedConnection = None return self.deleteItem(self.startedConnection) 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 deleteItem(self, item=None): if item: if type(item) is BlockItem: for p in item.inputs + item.outputs: if p.connection: self.deleteItem(p.connection) self.removeItem(item) elif type(item) is Connection: item.releasePorts() self.removeItem(item) else: # No item was supplied, try to delete all currently selected items: items = self.selectedItems() connections = [item for item in items if type(item) is Connection] blocks = [item for item in items if type(item) is BlockItem] for item in connections + blocks: self.deleteItem(item) class DiagramEditor(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) # Widget layout and child widgets: self.horizontalLayout = QHBoxLayout(self) self.diagramScene = DiagramScene(self) self.loadDiagram = self.diagramScene.loadDiagram self.diagramView = EditorGraphicsView(self.diagramScene, self) self.horizontalLayout.addWidget(self.diagramView) testShortcut = QShortcut(QKeySequence("F12"), self) testShortcut.activated.connect(self.test) delShort = QShortcut(QKeySequence.Delete, self) delShort.activated.connect(self.diagramScene.deleteItem) def test(self): self.diagramView.rotate(30) self.zoomAll() def save(self): self.diagramScene.saveDiagram('diagram2.usd') def load(self): filename = QFileDialog.getOpenFileName(self) self.diagramScene.loadDiagram(filename) def zoomAll(self): """ zoom to fit all items """ rect = self.diagramScene.itemsBoundingRect() self.diagramView.fitInView(rect, Qt.KeepAspectRatio) 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: self.libItems = [] self.libItems.append( QStandardItem(QIcon(pixmap), 'Block') ) self.libItems.append( QStandardItem(QIcon(pixmap), 'Uber Unit') ) self.libItems.append( QStandardItem(QIcon(pixmap), 'Device') ) for i in self.libItems: self.libraryModel.appendRow(i) self.setModel(self.libraryModel) self.setViewMode(self.IconMode) self.setDragDropMode(self.DragOnly) class Main(QMainWindow): def __init__(self): super(Main, self).__init__(None) self.editor = DiagramEditor() self.setCentralWidget(self.editor) self.setWindowTitle("Diagram editor") toolbar = self.addToolBar('Tools') saveAction = QAction('Save', self) saveAction.setShortcuts(QKeySequence.Save) saveAction.triggered.connect(self.editor.save) toolbar.addAction(saveAction) openAction = QAction('Open', self) openAction.setShortcuts(QKeySequence.Open) openAction.triggered.connect(self.editor.load) toolbar.addAction(openAction) fullScreenAction = QAction('Full screen', self) fullScreenAction.setShortcuts(QKeySequence("F11")) fullScreenAction.triggered.connect(self.toggleFullScreen) toolbar.addAction(fullScreenAction) zoomAction = QAction('Fit in view', self) zoomAction.setShortcuts(QKeySequence('F8')) zoomAction.triggered.connect(self.editor.zoomAll) toolbar.addAction(zoomAction) self.library = LibraryWidget() libraryDock = QDockWidget('Library', self) libraryDock.setWidget(self.library) self.addDockWidget(Qt.LeftDockWidgetArea, libraryDock) self.editor.loadDiagram('diagram2.usd') def toggleFullScreen(self): self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) self.editor.zoomAll() 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() main.resize(700, 500) main.editor.zoomAll() app.exec_()