Mercurial > lcfOS
diff python/apps/diagramitems.py @ 88:f3fe557be5ed
Split off of items to reduce file size
author | windel |
---|---|
date | Tue, 27 Nov 2012 18:00:13 +0100 |
parents | |
children | 4b1892054744 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/python/apps/diagramitems.py Tue Nov 27 18:00:13 2012 +0100 @@ -0,0 +1,413 @@ +from PyQt4.QtGui import * +from PyQt4.QtCore import * + +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): + """ 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 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 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.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 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()