Mercurial > lcfOS
view python/diagramitems.py @ 221:848c4b15fd0b
pointers
author | Windel Bouwman |
---|---|
date | Mon, 08 Jul 2013 22:21:44 +0200 |
parents | 6efbeb903777 |
children |
line wrap: on
line source
""" 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()