Mercurial > lcfOS
diff python/apps/diagrameditor.py @ 63:32078200cdd6
Several move action
author | windel |
---|---|
date | Sun, 07 Oct 2012 17:04:10 +0200 |
parents | python/lab/diagrameditor.py@fd7d5069734e |
children | b01311fb3be7 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/python/apps/diagrameditor.py Sun Oct 07 17:04:10 2012 +0200 @@ -0,0 +1,653 @@ +#!/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_() +