Mercurial > lcfOS
comparison 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 |
comparison
equal
deleted
inserted
replaced
290:7b38782ed496 | 292:534b94b40aa8 |
---|---|
1 """ | |
2 Contains all blocks that can be used to build models. | |
3 """ | |
4 | |
5 from PyQt4.QtGui import * | |
6 from PyQt4.QtCore import * | |
7 | |
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 | |
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): | |
31 """ A connection between blocks """ | |
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) | |
109 citems = [i for i in citems if type(i) is Block] | |
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) | |
142 def getDict(self): | |
143 return {'name': self.name} | |
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 """ | |
165 def __init__(self, dx=10.0, parent=None): | |
166 super(Handle, self).__init__(QRectF(-0.5*dx,-0.5*dx,dx,dx), parent) | |
167 self.setBrush(QBrush(Qt.white)) | |
168 self.setFlags(self.ItemIsMovable) | |
169 self.setZValue(1) | |
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): | |
178 def __init__(self, position, block): | |
179 super(ResizeSelectionHandle, self).__init__(dx=12, parent=block) | |
180 self.position = position | |
181 self.block = block | |
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): | |
191 self.block.sizerMoveEvent(self, event.scenePos()) | |
192 | |
193 class Block(QGraphicsRectItem): | |
194 """ Represents a block in the diagram. """ | |
195 def __init__(self, name='Untitled', parent=None): | |
196 super(Block, self).__init__(parent) | |
197 self.selectionHandles = [ResizeSelectionHandle(i, self) for i in range(8)] | |
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)) | |
203 self.setAcceptHoverEvents(True) | |
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 = [] | |
220 self.changeSize(2,2) | |
221 def editParameters(self): | |
222 pd = ParameterDialog(self, self.window()) | |
223 pd.exec_() | |
224 def newInputPort(self): | |
225 names = [i.name for i in self.inputs + self.outputs] | |
226 self.addInput(InputPort(uniqify('in', names), self)) | |
227 def newOutputPort(self): | |
228 names = [i.name for i in self.inputs + self.outputs] | |
229 self.addOutput(OutputPort(uniqify('out', names), self)) | |
230 def setName(self, name): self.label.setPlainText(name) | |
231 def getName(self): return self.label.toPlainText() | |
232 name = property(getName, setName) | |
233 def getDict(self): | |
234 d = {'x': self.scenePos().x(), 'y': self.scenePos().y()} | |
235 rect = self.rect() | |
236 d.update({'width': rect.width(), 'height': rect.height()}) | |
237 d['name'] = self.name | |
238 d['inputs'] = [inp.Dict for inp in self.inputs] | |
239 d['outputs'] = [outp.Dict for outp in self.outputs] | |
240 return d | |
241 def setDict(self, d): | |
242 self.name = d['name'] | |
243 self.setPos(d['x'], d['y']) | |
244 self.changeSize(d['width'], d['height']) | |
245 for inp in d['inputs']: | |
246 self.addInput(InputPort(inp['name'], self)) | |
247 for outp in d['outputs']: | |
248 self.addOutput(OutputPort(outp['name'], self)) | |
249 Dict = property(getDict, setDict) | |
250 def addInput(self, i): | |
251 self.inputs.append(i) | |
252 self.updateSize() | |
253 def addOutput(self, o): | |
254 self.outputs.append(o) | |
255 self.updateSize() | |
256 def contextMenuEvent(self, event): | |
257 menu = QMenu() | |
258 pa = menu.addAction('Parameters') | |
259 pa.triggered.connect(self.editParameters) | |
260 menu.exec_(event.screenPos()) | |
261 def itemChange(self, change, value): | |
262 if change == self.ItemSelectedHasChanged: | |
263 for child in [self.buttonItemAddInput, self.buttonItemAddOutput]: | |
264 child.setVisible(value) | |
265 if value: | |
266 self.repositionAndShowHandles() | |
267 else: | |
268 [h.setVisible(False) for h in self.selectionHandles] | |
269 | |
270 return super(Block, self).itemChange(change, value) | |
271 def hoverEnterEvent(self, event): | |
272 if not self.isSelected(): | |
273 self.repositionAndShowHandles() | |
274 super(Block, self).hoverEnterEvent(event) | |
275 def hoverLeaveEvent(self, event): | |
276 if not self.isSelected(): | |
277 [h.setVisible(False) for h in self.selectionHandles] | |
278 super(Block, self).hoverLeaveEvent(event) | |
279 def myDelete(self): | |
280 for p in self.inputs + self.outputs: | |
281 if p.connection: p.connection.myDelete() | |
282 self.scene().removeItem(self) | |
283 def repositionAndShowHandles(self): | |
284 r = self.rect() | |
285 self.selectionHandles[Position.TOP_LEFT].setPos(r.topLeft()) | |
286 self.selectionHandles[Position.TOP].setPos(r.center().x(), r.top()) | |
287 self.selectionHandles[Position.TOP_RIGHT].setPos(r.topRight()) | |
288 self.selectionHandles[Position.RIGHT].setPos(r.right(), r.center().y()) | |
289 self.selectionHandles[Position.BOTTOM_RIGHT].setPos(r.bottomRight()) | |
290 self.selectionHandles[Position.BOTTOM].setPos(r.center().x(), r.bottom()) | |
291 self.selectionHandles[Position.BOTTOM_LEFT].setPos(r.bottomLeft()) | |
292 self.selectionHandles[Position.LEFT].setPos(r.left(), r.center().y()) | |
293 for h in self.selectionHandles: | |
294 h.setVisible(True) | |
295 def sizerMoveEvent(self, handle, pos): | |
296 r = self.rect().translated(self.pos()) | |
297 if handle.position == Position.TOP_LEFT: r.setTopLeft(pos) | |
298 elif handle.position == Position.TOP: r.setTop(pos.y()) | |
299 elif handle.position == Position.TOP_RIGHT: r.setTopRight(pos) | |
300 elif handle.position == Position.RIGHT: r.setRight(pos.x()) | |
301 elif handle.position == Position.BOTTOM_RIGHT: r.setBottomRight(pos) | |
302 elif handle.position == Position.BOTTOM: r.setBottom(pos.y()) | |
303 elif handle.position == Position.BOTTOM_LEFT: r.setBottomLeft(pos) | |
304 elif handle.position == Position.LEFT: r.setLeft(pos.x()) | |
305 else: | |
306 print('invalid position') | |
307 self.setCenterAndSize(r.center(), r.size()) | |
308 self.repositionAndShowHandles() | |
309 def updateSize(self): | |
310 rect = self.rect() | |
311 h, w = rect.height(), rect.width() | |
312 self.buttonItemAddInput.setPos(0, h + 4) | |
313 self.buttonItemAddOutput.setPos(w+10, h+4) | |
314 for inp, y in zip(self.inputs, equalSpace(len(self.inputs), h)): | |
315 inp.setPos(0.0, y) | |
316 for outp, y in zip(self.outputs, equalSpace(len(self.outputs), h)): | |
317 outp.setPos(w, y) | |
318 def setCenterAndSize(self, center, size): | |
319 self.changeSize(size.width(), size.height()) | |
320 p = QPointF(size.width(), size.height()) | |
321 self.setPos(center - p / 2) | |
322 def changeSize(self, w, h): | |
323 minw = 150 | |
324 minh = 50 | |
325 h = minh if h < minh else h | |
326 w = minw if w < minw else w | |
327 self.setRect(0.0, 0.0, w, h) | |
328 rect = self.label.boundingRect() | |
329 self.label.setPos((w - rect.width()) / 2, (h - rect.height()) / 2) | |
330 self.updateSize() | |
331 | |
332 class CodeBlock(Block): | |
333 def __init__(self, name='Untitled', parent=None): | |
334 super(CodeBlock, self).__init__(name, parent) | |
335 self.code = '' | |
336 def setDict(self, d): | |
337 super(CodeBlock, self).setDict(d) | |
338 self.code = d['code'] | |
339 def getDict(self): | |
340 d = super(CodeBlock, self).getDict() | |
341 d['code'] = self.code | |
342 return d | |
343 def gencode(self): | |
344 c = ['def {0}():'.format(self.name)] | |
345 if self.code: | |
346 c += indent(self.code.split('\n')) | |
347 else: | |
348 c += indent(['pass']) | |
349 return c | |
350 | |
351 class DiagramBlock(Block): | |
352 def __init__(self, name='Untitled', parent=None): | |
353 super(DiagramBlock, self).__init__(name, parent) | |
354 self.subModel = DiagramScene() | |
355 self.subModel.containingBlock = self | |
356 def setDict(self, d): | |
357 self.subModel.Dict = d['submodel'] | |
358 def mouseDoubleClickEvent(self, event): | |
359 # descent into child diagram | |
360 #self.editParameters() | |
361 print('descent') | |
362 scene = self.scene() | |
363 if scene: | |
364 for view in scene.views(): | |
365 view.diagram = self.subModel | |
366 view.zoomAll() | |
367 | |
368 class DiagramScene(QGraphicsScene): | |
369 """ A diagram scene consisting of blocks and connections """ | |
370 structureChanged = pyqtSignal() | |
371 def __init__(self): | |
372 super(DiagramScene, self).__init__() | |
373 self.startedConnection = None | |
374 | |
375 blocks = property(lambda sel: [i for i in sel.items() if isinstance(i, Block)]) | |
376 connections = property(lambda sel: [i for i in sel.items() if type(i) is Connection]) | |
377 def addItem(self, item): | |
378 super(DiagramScene, self).addItem(item) | |
379 if isinstance(item, Block): | |
380 self.structureChanged.emit() | |
381 def removeItem(self, item): | |
382 super(DiagramScene, self).removeItem(item) | |
383 if isinstance(item, Block): | |
384 self.structureChanged.emit() | |
385 def setDict(self, d): | |
386 for block in d['blocks']: | |
387 b = Block() | |
388 self.addItem(b) | |
389 b.Dict = block | |
390 for con in d['connections']: | |
391 fromPort = self.findPort(con['fromBlock'], con['fromPort']) | |
392 toPort = self.findPort(con['toBlock'], con['toPort']) | |
393 self.addItem(Connection(fromPort, toPort)) | |
394 def getDict(self): | |
395 return {'blocks': [b.Dict for b in self.blocks], 'connections': [c.Dict for c in self.connections]} | |
396 Dict = property(getDict, setDict) | |
397 def gencode(self): | |
398 c = [] | |
399 for b in self.blocks: | |
400 c += b.gencode() | |
401 for b in self.blocks: | |
402 c.append('{0}()'.format(b.name)) | |
403 return c | |
404 def findPort(self, blockname, portname): | |
405 block = self.findBlock(blockname) | |
406 if block: | |
407 for port in block.inputs + block.outputs: | |
408 if port.name == portname: return port | |
409 def findBlock(self, blockname): | |
410 for block in self.blocks: | |
411 if block.name == blockname: return block | |
412 def uniqify(self, name): | |
413 blocknames = [item.name for item in self.blocks] | |
414 return uniqify(name, blocknames) | |
415 def mouseMoveEvent(self, event): | |
416 if self.startedConnection: | |
417 pos = event.scenePos() | |
418 self.startedConnection.setEndPos(pos) | |
419 super(DiagramScene, self).mouseMoveEvent(event) | |
420 def mouseReleaseEvent(self, event): | |
421 if self.startedConnection: | |
422 for item in self.items(event.scenePos()): | |
423 if type(item) is InputPort and item.connection == None: | |
424 self.startedConnection.setToPort(item) | |
425 self.startedConnection = None | |
426 return | |
427 self.startedConnection.myDelete() | |
428 self.startedConnection = None | |
429 super(DiagramScene, self).mouseReleaseEvent(event) | |
430 def startConnection(self, port): | |
431 self.startedConnection = Connection(port, None) | |
432 pos = port.scenePos() | |
433 self.startedConnection.setEndPos(pos) | |
434 self.addItem(self.startedConnection) | |
435 def deleteItems(self): | |
436 for item in list(self.selectedItems()): item.myDelete() | |
437 |