Mercurial > lcfOS
diff python/other/diagramitems.py @ 292:534b94b40aa8
Fixup reorganize
author | Windel Bouwman |
---|---|
date | Wed, 27 Nov 2013 08:06:42 +0100 |
parents | python/diagramitems.py@6efbeb903777 |
children | b77f3290ac79 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/python/other/diagramitems.py Wed Nov 27 08:06:42 2013 +0100 @@ -0,0 +1,437 @@ +""" + Contains all blocks that can be used to build models. +""" + +from PyQt4.QtGui import * +from PyQt4.QtCore import * + +def uniqify(name, names): + newname, i = name, 1 + while newname in names: newname, i = name + str(i), i + 1 + return newname + +def enum(**enums): + return type('Enum', (), enums) + +Position = enum(TOP=0, TOP_RIGHT=1, RIGHT=2, BOTTOM_RIGHT=3, BOTTOM=4, BOTTOM_LEFT=5, LEFT=6, TOP_LEFT=7) + +def buildPath(pts): + path = QPainterPath(pts[0]) + for pt in pts[1:]: path.lineTo(pt) + return path + +def equalSpace(n, l, offset=15): + if n == 1: + return [l / 2] + elif n > 1: + return [offset + (l - offset*2)/(n - 1)*i for i in range(n)] + return [] + +class Connection(QGraphicsPathItem): + """ A connection between blocks """ + def __init__(self, fromPort=None, toPort=None): + super(Connection, self).__init__() + self.pos2 = self.fromPort = self.toPort = None + self.setFlags(self.ItemIsSelectable | self.ItemClipsToShape) + pen = 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 Block] + scene.removeItem(litem) + pts = [pos1] + vias + [pos2] + self.arrowhead.setPos(pos2) + self.arrowhead.setRotation(90) + p = buildPath(pts) + self.setPath(p) + """ Create a shape outline using the path stroker """ + s = super(Connection, self).shape() + pps = QPainterPathStroker() + pps.setWidth(10) + self.myshape = pps.createStroke(s).simplified() + +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, dx=10.0, parent=None): + super(Handle, self).__init__(QRectF(-0.5*dx,-0.5*dx,dx,dx), parent) + self.setBrush(QBrush(Qt.white)) + self.setFlags(self.ItemIsMovable) + self.setZValue(1) + 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) + +class ResizeSelectionHandle(Handle): + def __init__(self, position, block): + super(ResizeSelectionHandle, self).__init__(dx=12, parent=block) + self.position = position + self.block = block + if position in [Position.TOP_LEFT, Position.BOTTOM_RIGHT]: + self.setCursor(QCursor(Qt.SizeFDiagCursor)) + elif position in [Position.TOP_RIGHT, Position.BOTTOM_LEFT]: + self.setCursor(QCursor(Qt.SizeBDiagCursor)) + elif position in [Position.TOP, Position.BOTTOM]: + self.setCursor(QCursor(Qt.SizeVerCursor)) + elif position in [Position.LEFT, Position.RIGHT]: + self.setCursor(QCursor(Qt.SizeHorCursor)) + def mouseMoveEvent(self, event): + self.block.sizerMoveEvent(self, event.scenePos()) + +class Block(QGraphicsRectItem): + """ Represents a block in the diagram. """ + def __init__(self, name='Untitled', parent=None): + super(Block, self).__init__(parent) + self.selectionHandles = [ResizeSelectionHandle(i, self) for i in range(8)] + # Properties of the rectangle: + self.setPen(QPen(Qt.blue, 2)) + self.setBrush(QBrush(Qt.lightGray)) + self.setFlags(self.ItemIsSelectable | self.ItemIsMovable | self.ItemSendsScenePositionChanges) + self.setCursor(QCursor(Qt.PointingHandCursor)) + self.setAcceptHoverEvents(True) + self.label = QGraphicsTextItem(name, self) + self.name = name + # 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 = [] + self.changeSize(2,2) + def editParameters(self): + pd = ParameterDialog(self, self.window()) + pd.exec_() + def newInputPort(self): + names = [i.name for i in self.inputs + self.outputs] + self.addInput(InputPort(uniqify('in', names), self)) + def newOutputPort(self): + names = [i.name for i in self.inputs + self.outputs] + self.addOutput(OutputPort(uniqify('out', names), self)) + def setName(self, name): self.label.setPlainText(name) + def getName(self): return self.label.toPlainText() + name = property(getName, setName) + def getDict(self): + d = {'x': self.scenePos().x(), 'y': self.scenePos().y()} + rect = self.rect() + d.update({'width': rect.width(), 'height': rect.height()}) + d['name'] = self.name + d['inputs'] = [inp.Dict for inp in self.inputs] + d['outputs'] = [outp.Dict for outp in self.outputs] + return d + def setDict(self, d): + self.name = d['name'] + self.setPos(d['x'], d['y']) + self.changeSize(d['width'], d['height']) + for inp in d['inputs']: + self.addInput(InputPort(inp['name'], self)) + for outp in d['outputs']: + self.addOutput(OutputPort(outp['name'], self)) + Dict = property(getDict, setDict) + def addInput(self, i): + self.inputs.append(i) + self.updateSize() + def addOutput(self, o): + self.outputs.append(o) + self.updateSize() + def contextMenuEvent(self, event): + menu = QMenu() + pa = menu.addAction('Parameters') + pa.triggered.connect(self.editParameters) + menu.exec_(event.screenPos()) + def itemChange(self, change, value): + if change == self.ItemSelectedHasChanged: + for child in [self.buttonItemAddInput, self.buttonItemAddOutput]: + child.setVisible(value) + if value: + self.repositionAndShowHandles() + else: + [h.setVisible(False) for h in self.selectionHandles] + + return super(Block, self).itemChange(change, value) + def hoverEnterEvent(self, event): + if not self.isSelected(): + self.repositionAndShowHandles() + super(Block, self).hoverEnterEvent(event) + def hoverLeaveEvent(self, event): + if not self.isSelected(): + [h.setVisible(False) for h in self.selectionHandles] + super(Block, self).hoverLeaveEvent(event) + def myDelete(self): + for p in self.inputs + self.outputs: + if p.connection: p.connection.myDelete() + self.scene().removeItem(self) + def repositionAndShowHandles(self): + r = self.rect() + self.selectionHandles[Position.TOP_LEFT].setPos(r.topLeft()) + self.selectionHandles[Position.TOP].setPos(r.center().x(), r.top()) + self.selectionHandles[Position.TOP_RIGHT].setPos(r.topRight()) + self.selectionHandles[Position.RIGHT].setPos(r.right(), r.center().y()) + self.selectionHandles[Position.BOTTOM_RIGHT].setPos(r.bottomRight()) + self.selectionHandles[Position.BOTTOM].setPos(r.center().x(), r.bottom()) + self.selectionHandles[Position.BOTTOM_LEFT].setPos(r.bottomLeft()) + self.selectionHandles[Position.LEFT].setPos(r.left(), r.center().y()) + for h in self.selectionHandles: + h.setVisible(True) + def sizerMoveEvent(self, handle, pos): + r = self.rect().translated(self.pos()) + if handle.position == Position.TOP_LEFT: r.setTopLeft(pos) + elif handle.position == Position.TOP: r.setTop(pos.y()) + elif handle.position == Position.TOP_RIGHT: r.setTopRight(pos) + elif handle.position == Position.RIGHT: r.setRight(pos.x()) + elif handle.position == Position.BOTTOM_RIGHT: r.setBottomRight(pos) + elif handle.position == Position.BOTTOM: r.setBottom(pos.y()) + elif handle.position == Position.BOTTOM_LEFT: r.setBottomLeft(pos) + elif handle.position == Position.LEFT: r.setLeft(pos.x()) + else: + print('invalid position') + self.setCenterAndSize(r.center(), r.size()) + self.repositionAndShowHandles() + def updateSize(self): + rect = self.rect() + h, w = rect.height(), rect.width() + self.buttonItemAddInput.setPos(0, h + 4) + self.buttonItemAddOutput.setPos(w+10, h+4) + for inp, y in zip(self.inputs, equalSpace(len(self.inputs), h)): + inp.setPos(0.0, y) + for outp, y in zip(self.outputs, equalSpace(len(self.outputs), h)): + outp.setPos(w, y) + def setCenterAndSize(self, center, size): + self.changeSize(size.width(), size.height()) + p = QPointF(size.width(), size.height()) + self.setPos(center - p / 2) + def changeSize(self, w, h): + minw = 150 + minh = 50 + h = minh if h < minh else h + w = minw if w < minw else w + self.setRect(0.0, 0.0, w, h) + rect = self.label.boundingRect() + self.label.setPos((w - rect.width()) / 2, (h - rect.height()) / 2) + self.updateSize() + +class CodeBlock(Block): + def __init__(self, name='Untitled', parent=None): + super(CodeBlock, self).__init__(name, parent) + self.code = '' + def setDict(self, d): + super(CodeBlock, self).setDict(d) + self.code = d['code'] + def getDict(self): + d = super(CodeBlock, self).getDict() + d['code'] = self.code + return d + def gencode(self): + c = ['def {0}():'.format(self.name)] + if self.code: + c += indent(self.code.split('\n')) + else: + c += indent(['pass']) + return c + +class DiagramBlock(Block): + def __init__(self, name='Untitled', parent=None): + super(DiagramBlock, self).__init__(name, parent) + self.subModel = DiagramScene() + self.subModel.containingBlock = self + def setDict(self, d): + self.subModel.Dict = d['submodel'] + def mouseDoubleClickEvent(self, event): + # descent into child diagram + #self.editParameters() + print('descent') + scene = self.scene() + if scene: + for view in scene.views(): + view.diagram = self.subModel + view.zoomAll() + +class DiagramScene(QGraphicsScene): + """ A diagram scene consisting of blocks and connections """ + structureChanged = pyqtSignal() + def __init__(self): + super(DiagramScene, self).__init__() + self.startedConnection = None + + blocks = property(lambda sel: [i for i in sel.items() if isinstance(i, Block)]) + connections = property(lambda sel: [i for i in sel.items() if type(i) is Connection]) + def addItem(self, item): + super(DiagramScene, self).addItem(item) + if isinstance(item, Block): + self.structureChanged.emit() + def removeItem(self, item): + super(DiagramScene, self).removeItem(item) + if isinstance(item, Block): + self.structureChanged.emit() + def setDict(self, d): + for block in d['blocks']: + b = Block() + self.addItem(b) + b.Dict = block + for con in d['connections']: + fromPort = self.findPort(con['fromBlock'], con['fromPort']) + toPort = self.findPort(con['toBlock'], con['toPort']) + self.addItem(Connection(fromPort, toPort)) + def getDict(self): + return {'blocks': [b.Dict for b in self.blocks], 'connections': [c.Dict for c in self.connections]} + Dict = property(getDict, setDict) + def gencode(self): + c = [] + for b in self.blocks: + c += b.gencode() + for b in self.blocks: + c.append('{0}()'.format(b.name)) + return c + def findPort(self, blockname, portname): + block = self.findBlock(blockname) + if block: + for port in block.inputs + block.outputs: + if port.name == portname: return port + def findBlock(self, blockname): + for block in self.blocks: + if block.name == blockname: return block + def uniqify(self, name): + blocknames = [item.name for item in self.blocks] + return uniqify(name, blocknames) + def mouseMoveEvent(self, event): + if self.startedConnection: + pos = event.scenePos() + self.startedConnection.setEndPos(pos) + super(DiagramScene, self).mouseMoveEvent(event) + def mouseReleaseEvent(self, event): + if self.startedConnection: + for item in self.items(event.scenePos()): + if type(item) is InputPort and item.connection == None: + self.startedConnection.setToPort(item) + self.startedConnection = None + return + self.startedConnection.myDelete() + self.startedConnection = None + super(DiagramScene, self).mouseReleaseEvent(event) + def startConnection(self, port): + self.startedConnection = Connection(port, None) + pos = port.scenePos() + self.startedConnection.setEndPos(pos) + self.addItem(self.startedConnection) + def deleteItems(self): + for item in list(self.selectedItems()): item.myDelete() +