92
|
1 """
|
|
2 Contains all blocks that can be used to build models.
|
|
3 """
|
|
4
|
88
|
5 from PyQt4.QtGui import *
|
|
6 from PyQt4.QtCore import *
|
|
7
|
91
|
8 def uniqify(name, names):
|
|
9 newname, i = name, 1
|
|
10 while newname in names: newname, i = name + str(i), i + 1
|
|
11 return newname
|
|
12
|
88
|
13 def enum(**enums):
|
|
14 return type('Enum', (), enums)
|
|
15
|
|
16 Position = enum(TOP=0, TOP_RIGHT=1, RIGHT=2, BOTTOM_RIGHT=3, BOTTOM=4, BOTTOM_LEFT=5, LEFT=6, TOP_LEFT=7)
|
|
17
|
|
18 def buildPath(pts):
|
|
19 path = QPainterPath(pts[0])
|
|
20 for pt in pts[1:]: path.lineTo(pt)
|
|
21 return path
|
|
22
|
|
23 def equalSpace(n, l, offset=15):
|
|
24 if n == 1:
|
|
25 return [l / 2]
|
|
26 elif n > 1:
|
|
27 return [offset + (l - offset*2)/(n - 1)*i for i in range(n)]
|
|
28 return []
|
|
29
|
|
30 class Connection(QGraphicsPathItem):
|
92
|
31 """ A connection between blocks """
|
88
|
32 def __init__(self, fromPort=None, toPort=None):
|
|
33 super(Connection, self).__init__()
|
|
34 self.pos2 = self.fromPort = self.toPort = None
|
|
35 self.setFlags(self.ItemIsSelectable | self.ItemClipsToShape)
|
|
36 pen = QPen(Qt.blue, 2, cap=Qt.RoundCap)
|
|
37 self.setPen(pen)
|
|
38 self.arrowhead = QGraphicsPathItem(self)
|
|
39 self.arrowhead.setPath(buildPath([QPointF(0.0, 0.0), QPointF(-6.0, 10.0), QPointF(6.0, 10.0), QPointF(0.0, 0.0)]))
|
|
40 self.arrowhead.setPen(pen)
|
|
41 self.arrowhead.setBrush(QBrush(pen.color()))
|
|
42 self.vias = []
|
|
43 self.setFromPort(fromPort)
|
|
44 self.setToPort(toPort)
|
|
45 def getDict(self):
|
|
46 d = {}
|
|
47 d['fromBlock'] = self.fromPort.block.name
|
|
48 d['fromPort'] = self.fromPort.name
|
|
49 d['toBlock'] = self.toPort.block.name
|
|
50 d['toPort'] = self.toPort.name
|
|
51 return d
|
|
52 Dict = property(getDict)
|
|
53 def myDelete(self):
|
|
54 scene = self.scene()
|
|
55 if scene:
|
|
56 self.setFromPort(None)
|
|
57 self.setToPort(None)
|
|
58 scene.removeItem(self)
|
|
59 def setFromPort(self, fromPort):
|
|
60 if self.fromPort:
|
|
61 self.fromPort.posCallbacks.remove(self.setBeginPos)
|
|
62 self.fromPort.connection = None
|
|
63 self.fromPort = fromPort
|
|
64 if self.fromPort:
|
|
65 self.fromPort.connection = self
|
|
66 self.updateLineStukken()
|
|
67 self.fromPort.posCallbacks.append(self.setBeginPos)
|
|
68 def setToPort(self, toPort):
|
|
69 if self.toPort:
|
|
70 self.toPort.posCallbacks.remove(self.setEndPos)
|
|
71 self.toPort.connection = None
|
|
72 self.toPort = toPort
|
|
73 if self.toPort:
|
|
74 self.setEndPos(toPort.scenePos())
|
|
75 self.toPort.connection = self
|
|
76 self.toPort.posCallbacks.append(self.setEndPos)
|
|
77 def getPos1(self):
|
|
78 if self.fromPort:
|
|
79 return self.fromPort.scenePos()
|
|
80 def setBeginPos(self, pos1): self.updateLineStukken()
|
|
81 def setEndPos(self, endpos):
|
|
82 self.pos2 = endpos
|
|
83 self.updateLineStukken()
|
|
84 def itemChange(self, change, value):
|
|
85 if change == self.ItemSelectedHasChanged:
|
|
86 for via in self.vias:
|
|
87 via.setVisible(value)
|
|
88 return super(Connection, self).itemChange(change, value)
|
|
89 def shape(self): return self.myshape
|
|
90 def updateLineStukken(self):
|
|
91 """
|
|
92 This algorithm determines the optimal routing of all signals.
|
|
93 TODO: implement nice automatic line router
|
|
94 """
|
|
95 pos1 = self.getPos1()
|
|
96 pos2 = self.pos2
|
|
97 if pos1 is None or pos2 is None:
|
|
98 return
|
|
99 scene = self.scene()
|
|
100 vias = [pos1 + QPointF(20, 0)] + self.vias + [pos2 + QPointF(-20, 0)]
|
|
101 if scene:
|
|
102 litem = QGraphicsLineItem()
|
|
103 litem.setFlags(self.ItemIsSelectable)
|
|
104 scene.addItem(litem)
|
|
105 for p1, p2 in zip(vias[:-1], vias[1:]):
|
|
106 line = QLineF(p1, p2)
|
|
107 litem.setLine(line)
|
|
108 citems = scene.collidingItems(litem)
|
89
|
109 citems = [i for i in citems if type(i) is Block]
|
88
|
110 scene.removeItem(litem)
|
|
111 pts = [pos1] + vias + [pos2]
|
|
112 self.arrowhead.setPos(pos2)
|
|
113 self.arrowhead.setRotation(90)
|
|
114 p = buildPath(pts)
|
|
115 self.setPath(p)
|
|
116 """ Create a shape outline using the path stroker """
|
|
117 s = super(Connection, self).shape()
|
|
118 pps = QPainterPathStroker()
|
|
119 pps.setWidth(10)
|
|
120 self.myshape = pps.createStroke(s).simplified()
|
|
121
|
|
122 class PortItem(QGraphicsPathItem):
|
|
123 """ Represents a port to a subsystem """
|
|
124 def __init__(self, name, block):
|
|
125 super(PortItem, self).__init__(block)
|
|
126 self.textItem = QGraphicsTextItem(self)
|
|
127 self.connection = None
|
|
128 self.block = block
|
|
129 self.setCursor(QCursor(Qt.CrossCursor))
|
|
130 self.setPen(QPen(Qt.blue, 2, cap=Qt.RoundCap))
|
|
131 self.name = name
|
|
132 self.posCallbacks = []
|
|
133 self.setFlag(self.ItemSendsScenePositionChanges, True)
|
|
134 def getName(self): return self.textItem.toPlainText()
|
|
135 def setName(self, name):
|
|
136 self.textItem.setPlainText(name)
|
|
137 rect = self.textItem.boundingRect()
|
|
138 lw, lh = rect.width(), rect.height()
|
|
139 lx = 3 if type(self) is InputPort else -3 - lw
|
|
140 self.textItem.setPos(lx, -lh / 2)
|
|
141 name = property(getName, setName)
|
91
|
142 def getDict(self):
|
|
143 return {'name': self.name}
|
88
|
144 Dict = property(getDict)
|
|
145 def itemChange(self, change, value):
|
|
146 if change == self.ItemScenePositionHasChanged:
|
|
147 for cb in self.posCallbacks: cb(value)
|
|
148 return value
|
|
149 return super(PortItem, self).itemChange(change, value)
|
|
150
|
|
151 class OutputPort(PortItem):
|
|
152 def __init__(self, name, block, d=10.0):
|
|
153 super(OutputPort, self).__init__(name, block)
|
|
154 self.setPath(buildPath([QPointF(0.0, -d), QPointF(d, 0), QPointF(0.0, d)]))
|
|
155 def mousePressEvent(self, event):
|
|
156 self.scene().startConnection(self)
|
|
157
|
|
158 class InputPort(PortItem):
|
|
159 def __init__(self, name, block, d=10.0):
|
|
160 super(InputPort, self).__init__(name, block)
|
|
161 self.setPath(buildPath([QPointF(-d, -d), QPointF(0, 0), QPointF(-d, d)]))
|
|
162
|
|
163 class Handle(QGraphicsEllipseItem):
|
|
164 """ A handle that can be moved by the mouse """
|
89
|
165 def __init__(self, dx=10.0, parent=None):
|
88
|
166 super(Handle, self).__init__(QRectF(-0.5*dx,-0.5*dx,dx,dx), parent)
|
|
167 self.setBrush(QBrush(Qt.white))
|
90
|
168 self.setFlags(self.ItemIsMovable)
|
|
169 self.setZValue(1)
|
88
|
170 self.setVisible(False)
|
|
171 self.setCursor(QCursor(Qt.SizeFDiagCursor))
|
|
172 def mouseMoveEvent(self, event):
|
|
173 """ Move function without moving the other selected elements """
|
|
174 p = self.mapToParent(event.pos())
|
|
175 self.setPos(p)
|
|
176
|
|
177 class ResizeSelectionHandle(Handle):
|
90
|
178 def __init__(self, position, block):
|
|
179 super(ResizeSelectionHandle, self).__init__(dx=6, parent=block)
|
88
|
180 self.position = position
|
90
|
181 self.block = block
|
88
|
182 if position in [Position.TOP_LEFT, Position.BOTTOM_RIGHT]:
|
|
183 self.setCursor(QCursor(Qt.SizeFDiagCursor))
|
|
184 elif position in [Position.TOP_RIGHT, Position.BOTTOM_LEFT]:
|
|
185 self.setCursor(QCursor(Qt.SizeBDiagCursor))
|
|
186 elif position in [Position.TOP, Position.BOTTOM]:
|
|
187 self.setCursor(QCursor(Qt.SizeVerCursor))
|
|
188 elif position in [Position.LEFT, Position.RIGHT]:
|
|
189 self.setCursor(QCursor(Qt.SizeHorCursor))
|
|
190 def mouseMoveEvent(self, event):
|
90
|
191 self.block.sizerMoveEvent(self, event.scenePos())
|
88
|
192
|
89
|
193 class Block(QGraphicsRectItem):
|
92
|
194 """ Represents a block in the diagram. """
|
88
|
195 def __init__(self, name='Untitled', parent=None):
|
89
|
196 super(Block, self).__init__(parent)
|
90
|
197 self.selectionHandles = [ResizeSelectionHandle(i, self) for i in range(8)]
|
88
|
198 # Properties of the rectangle:
|
|
199 self.setPen(QPen(Qt.blue, 2))
|
|
200 self.setBrush(QBrush(Qt.lightGray))
|
|
201 self.setFlags(self.ItemIsSelectable | self.ItemIsMovable | self.ItemSendsScenePositionChanges)
|
|
202 self.setCursor(QCursor(Qt.PointingHandCursor))
|
92
|
203 self.setAcceptHoverEvents(True)
|
88
|
204 self.label = QGraphicsTextItem(name, self)
|
|
205 self.name = name
|
|
206 # Create corner for resize:
|
|
207 button = QPushButton('+in')
|
|
208 button.clicked.connect(self.newInputPort)
|
|
209 self.buttonItemAddInput = QGraphicsProxyWidget(self)
|
|
210 self.buttonItemAddInput.setWidget(button)
|
|
211 self.buttonItemAddInput.setVisible(False)
|
|
212 button = QPushButton('+out')
|
|
213 button.clicked.connect(self.newOutputPort)
|
|
214 self.buttonItemAddOutput = QGraphicsProxyWidget(self)
|
|
215 self.buttonItemAddOutput.setWidget(button)
|
|
216 self.buttonItemAddOutput.setVisible(False)
|
|
217 # Inputs and outputs of the block:
|
|
218 self.inputs = []
|
|
219 self.outputs = []
|
91
|
220 self.changeSize(2,2)
|
88
|
221 def editParameters(self):
|
|
222 pd = ParameterDialog(self, self.window())
|
|
223 pd.exec_()
|
|
224 def mouseDoubleClickEvent(self, event):
|
|
225 #self.editParameters()
|
|
226 scene = self.scene()
|
|
227 if scene:
|
|
228 for view in scene.views():
|
|
229 view.diagram = self.subModel
|
|
230 view.zoomAll()
|
|
231 def newInputPort(self):
|
|
232 names = [i.name for i in self.inputs + self.outputs]
|
|
233 self.addInput(InputPort(uniqify('in', names), self))
|
|
234 def newOutputPort(self):
|
|
235 names = [i.name for i in self.inputs + self.outputs]
|
|
236 self.addOutput(OutputPort(uniqify('out', names), self))
|
|
237 def setName(self, name): self.label.setPlainText(name)
|
|
238 def getName(self): return self.label.toPlainText()
|
|
239 name = property(getName, setName)
|
|
240 def getDict(self):
|
|
241 d = {'x': self.scenePos().x(), 'y': self.scenePos().y()}
|
|
242 rect = self.rect()
|
|
243 d.update({'width': rect.width(), 'height': rect.height()})
|
91
|
244 d['name'] = self.name
|
88
|
245 d['inputs'] = [inp.Dict for inp in self.inputs]
|
|
246 d['outputs'] = [outp.Dict for outp in self.outputs]
|
|
247 return d
|
|
248 def setDict(self, d):
|
|
249 self.name = d['name']
|
|
250 self.setPos(d['x'], d['y'])
|
|
251 self.changeSize(d['width'], d['height'])
|
92
|
252 for inp in d['inputs']:
|
|
253 self.addInput(InputPort(inp['name'], self))
|
|
254 for outp in d['outputs']:
|
|
255 self.addOutput(OutputPort(outp['name'], self))
|
88
|
256 Dict = property(getDict, setDict)
|
|
257 def addInput(self, i):
|
|
258 self.inputs.append(i)
|
|
259 self.updateSize()
|
|
260 def addOutput(self, o):
|
|
261 self.outputs.append(o)
|
|
262 self.updateSize()
|
|
263 def contextMenuEvent(self, event):
|
|
264 menu = QMenu()
|
|
265 pa = menu.addAction('Parameters')
|
|
266 pa.triggered.connect(self.editParameters)
|
|
267 menu.exec_(event.screenPos())
|
|
268 def itemChange(self, change, value):
|
|
269 if change == self.ItemSelectedHasChanged:
|
|
270 for child in [self.buttonItemAddInput, self.buttonItemAddOutput]:
|
|
271 child.setVisible(value)
|
90
|
272 if value:
|
|
273 self.repositionAndShowHandles()
|
|
274 else:
|
|
275 [h.setVisible(False) for h in self.selectionHandles]
|
|
276
|
89
|
277 return super(Block, self).itemChange(change, value)
|
91
|
278 def hoverEnterEvent(self, event):
|
92
|
279 if not self.isSelected():
|
|
280 self.repositionAndShowHandles()
|
91
|
281 super(Block, self).hoverEnterEvent(event)
|
|
282 def hoverLeaveEvent(self, event):
|
92
|
283 if not self.isSelected():
|
|
284 [h.setVisible(False) for h in self.selectionHandles]
|
91
|
285 super(Block, self).hoverLeaveEvent(event)
|
88
|
286 def myDelete(self):
|
|
287 for p in self.inputs + self.outputs:
|
|
288 if p.connection: p.connection.myDelete()
|
|
289 self.scene().removeItem(self)
|
90
|
290 def repositionAndShowHandles(self):
|
|
291 r = self.rect()
|
|
292 self.selectionHandles[Position.TOP_LEFT].setPos(r.topLeft())
|
|
293 self.selectionHandles[Position.TOP].setPos(r.center().x(), r.top())
|
|
294 self.selectionHandles[Position.TOP_RIGHT].setPos(r.topRight())
|
|
295 self.selectionHandles[Position.RIGHT].setPos(r.right(), r.center().y())
|
|
296 self.selectionHandles[Position.BOTTOM_RIGHT].setPos(r.bottomRight())
|
|
297 self.selectionHandles[Position.BOTTOM].setPos(r.center().x(), r.bottom())
|
|
298 self.selectionHandles[Position.BOTTOM_LEFT].setPos(r.bottomLeft())
|
|
299 self.selectionHandles[Position.LEFT].setPos(r.left(), r.center().y())
|
|
300 for h in self.selectionHandles:
|
|
301 h.setVisible(True)
|
|
302 def sizerMoveEvent(self, handle, pos):
|
|
303 r = self.rect().translated(self.pos())
|
|
304 if handle.position == Position.TOP_LEFT: r.setTopLeft(pos)
|
|
305 elif handle.position == Position.TOP: r.setTop(pos.y())
|
|
306 elif handle.position == Position.TOP_RIGHT: r.setTopRight(pos)
|
|
307 elif handle.position == Position.RIGHT: r.setRight(pos.x())
|
|
308 elif handle.position == Position.BOTTOM_RIGHT: r.setBottomRight(pos)
|
|
309 elif handle.position == Position.BOTTOM: r.setBottom(pos.y())
|
|
310 elif handle.position == Position.BOTTOM_LEFT: r.setBottomLeft(pos)
|
|
311 elif handle.position == Position.LEFT: r.setLeft(pos.x())
|
|
312 else:
|
|
313 print('invalid position')
|
|
314 self.setCenterAndSize(r.center(), r.size())
|
|
315 self.repositionAndShowHandles()
|
88
|
316 def updateSize(self):
|
|
317 rect = self.rect()
|
|
318 h, w = rect.height(), rect.width()
|
|
319 self.buttonItemAddInput.setPos(0, h + 4)
|
|
320 self.buttonItemAddOutput.setPos(w+10, h+4)
|
|
321 for inp, y in zip(self.inputs, equalSpace(len(self.inputs), h)):
|
|
322 inp.setPos(0.0, y)
|
|
323 for outp, y in zip(self.outputs, equalSpace(len(self.outputs), h)):
|
|
324 outp.setPos(w, y)
|
|
325 def setCenterAndSize(self, center, size):
|
|
326 self.changeSize(size.width(), size.height())
|
|
327 p = QPointF(size.width(), size.height())
|
|
328 self.setPos(center - p / 2)
|
|
329 def changeSize(self, w, h):
|
|
330 h = 20 if h < 20 else h
|
|
331 w = 40 if w < 40 else w
|
|
332 self.setRect(0.0, 0.0, w, h)
|
|
333 rect = self.label.boundingRect()
|
|
334 self.label.setPos((w - rect.width()) / 2, (h - rect.height()) / 2)
|
|
335 self.updateSize()
|
|
336
|
90
|
337 class CodeBlock(Block):
|
|
338 def __init__(self, name='Untitled', parent=None):
|
|
339 super(CodeBlock, self).__init__(name, parent)
|
|
340 self.code = ''
|
|
341 def setDict(self, d):
|
92
|
342 super(CodeBlock, self).setDict(d)
|
90
|
343 self.code = d['code']
|
92
|
344 def getDict(self):
|
|
345 d = super(CodeBlock, self).getDict()
|
|
346 d['code'] = self.code
|
|
347 return d
|
90
|
348 def gencode(self):
|
|
349 c = ['def {0}():'.format(self.name)]
|
|
350 if self.code:
|
|
351 c += indent(self.code.split('\n'))
|
|
352 else:
|
|
353 c += indent(['pass'])
|
|
354 return c
|
|
355
|
|
356 class DiagramBlock(Block):
|
|
357 def __init__(self, name='Untitled', parent=None):
|
|
358 super(DiagramBlock, self).__init__(name, parent)
|
|
359 self.subModel = DiagramScene()
|
|
360 self.subModel.containingBlock = self
|
|
361 def setDict(self, d):
|
|
362 self.subModel.Dict = d['submodel']
|
|
363
|
88
|
364 class DiagramScene(QGraphicsScene):
|
92
|
365 """ A diagram scene consisting of blocks and connections """
|
88
|
366 def __init__(self):
|
|
367 super(DiagramScene, self).__init__()
|
|
368 self.startedConnection = None
|
|
369
|
92
|
370 blocks = property(lambda sel: [i for i in sel.items() if isinstance(i, Block)])
|
88
|
371 connections = property(lambda sel: [i for i in sel.items() if type(i) is Connection])
|
|
372 def setDict(self, d):
|
|
373 for block in d['blocks']:
|
89
|
374 b = Block()
|
88
|
375 self.addItem(b)
|
|
376 b.Dict = block
|
|
377 for con in d['connections']:
|
|
378 fromPort = self.findPort(con['fromBlock'], con['fromPort'])
|
|
379 toPort = self.findPort(con['toBlock'], con['toPort'])
|
|
380 self.addItem(Connection(fromPort, toPort))
|
|
381 def getDict(self):
|
|
382 return {'blocks': [b.Dict for b in self.blocks], 'connections': [c.Dict for c in self.connections]}
|
|
383 Dict = property(getDict, setDict)
|
|
384 def gencode(self):
|
|
385 c = []
|
|
386 for b in self.blocks:
|
|
387 c += b.gencode()
|
|
388 for b in self.blocks:
|
|
389 c.append('{0}()'.format(b.name))
|
|
390 return c
|
|
391 def findPort(self, blockname, portname):
|
|
392 block = self.findBlock(blockname)
|
|
393 if block:
|
|
394 for port in block.inputs + block.outputs:
|
|
395 if port.name == portname: return port
|
|
396 def findBlock(self, blockname):
|
|
397 for block in self.blocks:
|
|
398 if block.name == blockname: return block
|
91
|
399 def uniqify(self, name):
|
88
|
400 blocknames = [item.name for item in self.blocks]
|
91
|
401 return uniqify(name, blocknames)
|
88
|
402 def mouseMoveEvent(self, event):
|
|
403 if self.startedConnection:
|
|
404 pos = event.scenePos()
|
|
405 self.startedConnection.setEndPos(pos)
|
|
406 super(DiagramScene, self).mouseMoveEvent(event)
|
|
407 def mouseReleaseEvent(self, event):
|
|
408 if self.startedConnection:
|
|
409 for item in self.items(event.scenePos()):
|
|
410 if type(item) is InputPort and item.connection == None:
|
|
411 self.startedConnection.setToPort(item)
|
|
412 self.startedConnection = None
|
|
413 return
|
|
414 self.startedConnection.myDelete()
|
|
415 self.startedConnection = None
|
|
416 super(DiagramScene, self).mouseReleaseEvent(event)
|
|
417 def startConnection(self, port):
|
|
418 self.startedConnection = Connection(port, None)
|
|
419 pos = port.scenePos()
|
|
420 self.startedConnection.setEndPos(pos)
|
|
421 self.addItem(self.startedConnection)
|
|
422 def deleteItems(self):
|
|
423 for item in list(self.selectedItems()): item.myDelete()
|
89
|
424
|