Mercurial > lcfOS
comparison applications/lab/diagrameditor.py @ 55:9414bad225a8
Added nice resizing
author | windel |
---|---|
date | Thu, 19 Apr 2012 18:16:34 +0200 |
parents | d8163d2c3779 |
children | 7a82f0d52e85 |
comparison
equal
deleted
inserted
replaced
54:d8163d2c3779 | 55:9414bad225a8 |
---|---|
11 Description: This script implements a diagram editor. | 11 Description: This script implements a diagram editor. |
12 run with python 3.x as: | 12 run with python 3.x as: |
13 $ python [thisfile.py] | 13 $ python [thisfile.py] |
14 """ | 14 """ |
15 | 15 |
16 class ArrowHead(QGraphicsPathItem): | |
17 def __init__(self, parent): | |
18 super(ArrowHead, self).__init__(parent) | |
19 arrowPath = QPainterPath(QPointF(0.0, 0.0)) | |
20 arrowPath.lineTo(-6.0, 10.0) | |
21 arrowPath.lineTo(6.0, 10.0) | |
22 arrowPath.lineTo(0.0, 0.0) | |
23 self.setPath(arrowPath) | |
24 pen = QPen(Qt.blue, 2) | |
25 self.setPen(pen) | |
26 self.setBrush(QBrush(pen.color())) | |
27 | |
28 class LinePart(QGraphicsLineItem): | |
29 def __init__(self, x1, y1, x2, y2): | |
30 super(LinePart, self).__init__(x1, y1, x2, y2) | |
31 self.setFlags(self.ItemIsSelectable) | |
32 def boundingRect(self): | |
33 rect = super(LinePart, self).boundingRect() | |
34 rect.adjust(-2, -2, 2, 2) | |
35 return rect | |
36 | |
37 #class Connection(QGraphicsItemGroup): | |
16 class Connection(QGraphicsPathItem): | 38 class Connection(QGraphicsPathItem): |
17 """ Implementation of a connection between blocks """ | 39 """ Implementation of a connection between blocks """ |
18 def __init__(self, fromPort, toPort): | 40 def __init__(self, fromPort, toPort): |
19 super(Connection, self).__init__() | 41 super(Connection, self).__init__() |
20 self.pos1 = None | 42 self.pos1 = None |
21 self.pos2 = None | 43 self.pos2 = None |
22 self.fromPort = None | 44 self.fromPort = None |
23 self.toPort = None | 45 self.toPort = None |
24 #self.setFlags(self.ItemIsSelectable | self.ItemIsMovable) | 46 #self.setFlags(self.ItemIsSelectable | self.ItemIsMovable) |
25 self.setFlags(self.ItemIsSelectable) | 47 self.setFlag(self.ItemIsSelectable, True) |
26 pen = QPen(Qt.blue, 2) | 48 self.setFlag(self.ItemClipsToShape, True) |
27 pen.setCapStyle(Qt.RoundCap) | 49 self.pen = QPen(Qt.blue, 2) |
28 self.setPen(pen) | 50 self.pen.setCapStyle(Qt.RoundCap) |
29 arrowPath = QPainterPath(QPointF(0.0, 0.0)) | 51 self.setPen(self.pen) |
30 arrowPath.lineTo(-6.0, 10.0) | 52 self.arrowhead = ArrowHead(self) |
31 arrowPath.lineTo(6.0, 10.0) | 53 #self.addToGroup(self.arrowhead) |
32 arrowPath.lineTo(0.0, 0.0) | 54 #self.body = QGraphicsPathItem(self) |
33 self.arrowhead = QGraphicsPathItem(arrowPath, self) | 55 #self.body.setPen(self.pen) |
34 self.arrowhead.setPen(pen) | 56 #self.addToGroup(self.body) |
35 self.arrowhead.setBrush(QBrush(pen.color())) | 57 self.lineItems = [] |
36 self.setFromPort(fromPort) | 58 self.setFromPort(fromPort) |
37 self.setToPort(toPort) | 59 self.setToPort(toPort) |
38 #def shape(self): | |
39 # return self.path() | |
40 def setFromPort(self, fromPort): | 60 def setFromPort(self, fromPort): |
41 if self.fromPort: | 61 if self.fromPort: |
42 self.fromPort.posCallbacks.remove(self.setBeginPos) | 62 self.fromPort.posCallbacks.remove(self.setBeginPos) |
43 self.fromPort.connection = None | 63 self.fromPort.connection = None |
44 self.fromPort = fromPort | 64 self.fromPort = fromPort |
62 self.pos1 = pos1 | 82 self.pos1 = pos1 |
63 self.updateLineStukken() | 83 self.updateLineStukken() |
64 def setEndPos(self, endpos): | 84 def setEndPos(self, endpos): |
65 self.pos2 = endpos | 85 self.pos2 = endpos |
66 self.updateLineStukken() | 86 self.updateLineStukken() |
87 def itemChange(self, change, value): | |
88 if change == self.ItemSelectedHasChanged: | |
89 pass # TODO, do something useful here. | |
90 return super(Connection, self).itemChange(change, value) | |
67 def updateLineStukken(self): | 91 def updateLineStukken(self): |
68 """ | 92 """ |
69 This algorithm determines the optimal routing of all signals. | 93 This algorithm determines the optimal routing of all signals. |
70 TODO: implement nice automatic line router | 94 TODO: implement nice automatic line router |
71 """ | 95 """ |
72 if self.pos1 is None or self.pos2 is None: | 96 if self.pos1 is None or self.pos2 is None: |
73 return | 97 return |
98 | |
74 # TODO: do not get the scene here? | 99 # TODO: do not get the scene here? |
75 ds = editor.diagramScene | 100 # TODO: create pieces of lines. |
101 | |
102 # Determine the begin and end positions: | |
103 pts = editor.diagramScene.vias(self.pos1, self.pos2) | |
104 | |
76 self.arrowhead.setPos(self.pos2) | 105 self.arrowhead.setPos(self.pos2) |
77 | 106 if pts[-1].x() < pts[-2].x(): |
78 # TODO: create pieces of lines. | 107 self.arrowhead.setRotation(-90) |
79 | |
80 # Determine the begin and end positions: | |
81 p1 = self.pos1 | |
82 p4 = self.pos2 | |
83 x1, y1 = self.pos1.x(), self.pos1.y() | |
84 x4, y4 = self.pos2.x(), self.pos2.y() | |
85 | |
86 def stripHits(hits): | |
87 """ Helper function that removes object hits """ | |
88 hits = [hit for hit in hits if type(hit) is BlockItem] | |
89 if self in hits: | |
90 hits.remove(self) | |
91 if self.fromPort in hits: | |
92 hits.remove(self.fromPort) | |
93 if self.toPort in hits: | |
94 hits.remove(self.toPort) | |
95 return hits | |
96 | |
97 def viaPoints(pA, pB): | |
98 # Construct intermediate points: | |
99 | |
100 pAB1 = QPointF(pA.x(), pB.y()) | |
101 pAB2 = QPointF(pB.x(), pA.y()) | |
102 path1 = QPainterPath(pA) | |
103 path1.lineTo(pAB1) | |
104 path2 = QPainterPath(pA) | |
105 path2.lineTo(pAB2) | |
106 if len(stripHits(ds.items(path1))) > len(stripHits(ds.items(path2))): | |
107 pAB = pAB2 | |
108 else: | |
109 pAB = pAB1 | |
110 return [pAB] | |
111 | |
112 # Determine left or right: | |
113 dx = QPointF(20, 0) | |
114 if len(stripHits(ds.items(p1 + dx))) > len(stripHits(ds.items(p1 - dx))): | |
115 p2 = p1 - dx | |
116 else: | 108 else: |
117 p2 = p1 + dx | |
118 | |
119 if len(stripHits(ds.items(p4 + dx))) > len(stripHits(ds.items(p4 - dx))): | |
120 p3 = p4 - dx | |
121 self.arrowhead.setRotation(90) | 109 self.arrowhead.setRotation(90) |
110 | |
111 usePath = True | |
112 if usePath: | |
113 path = QPainterPath(pts[0]) | |
114 # Now move from p2 to p3 without hitting blocks: | |
115 for pt in pts[1:]: | |
116 path.lineTo(pt) | |
117 for pt in reversed(pts[0:-1]): | |
118 path.lineTo(pt) | |
119 path.closeSubpath() | |
120 | |
121 #self.body.setPath(path) | |
122 self.setPath(path) | |
123 #outline = QGraphicsPathItem(self.shape()) | |
124 #editor.diagramScene.addItem(outline) | |
122 else: | 125 else: |
123 p3 = p4 + dx | 126 # Delete old line items: |
124 self.arrowhead.setRotation(-90) | 127 for li in self.lineItems: |
125 | 128 editor.diagramScene.removeItem(li) |
126 path = QPainterPath(p1) | 129 #self.removeFromGroup(li) |
127 path.lineTo(p2) | 130 self.lineItems = [] |
128 # Now move from p2 to p3 without hitting blocks: | 131 for a, b in zip(pts[0:-1], pts[1:]): |
129 pts = viaPoints(p2, p3) | 132 li = LinePart(a.x(),a.y(),b.x(),b.y()) |
130 for pt in pts: | 133 #self.addToGroup(li) |
131 path.lineTo(pt) | 134 editor.diagramScene.addItem(li) |
132 path.lineTo(p3) | 135 self.lineItems.append(li) |
133 path.lineTo(p4) | |
134 | |
135 hits = stripHits(ds.items(path)) | |
136 self.setPath(path) | |
137 | 136 |
138 class ParameterDialog(QDialog): | 137 class ParameterDialog(QDialog): |
139 def __init__(self, block, parent = None): | 138 def __init__(self, block, parent = None): |
140 super(ParameterDialog, self).__init__(parent) | 139 super(ParameterDialog, self).__init__(parent) |
141 self.block = block | 140 self.block = block |
194 self.name = name | 193 self.name = name |
195 self.textItem = QGraphicsTextItem(name, self) | 194 self.textItem = QGraphicsTextItem(name, self) |
196 self.setName(name) | 195 self.setName(name) |
197 self.posCallbacks = [] | 196 self.posCallbacks = [] |
198 self.setFlag(self.ItemSendsScenePositionChanges, True) | 197 self.setFlag(self.ItemSendsScenePositionChanges, True) |
198 def boundingRect(self): | |
199 rect = super(PortItem, self).boundingRect() | |
200 dx = 8 | |
201 rect.adjust(-dx, -dx, dx, dx) | |
202 return rect | |
199 def setName(self, name): | 203 def setName(self, name): |
200 self.name = name | 204 self.name = name |
201 self.textItem.setPlainText(name) | 205 self.textItem.setPlainText(name) |
202 rect = self.textItem.boundingRect() | 206 rect = self.textItem.boundingRect() |
203 lw, lh = rect.width(), rect.height() | 207 lw, lh = rect.width(), rect.height() |
222 | 226 |
223 # Block part: | 227 # Block part: |
224 class HandleItem(QGraphicsEllipseItem): | 228 class HandleItem(QGraphicsEllipseItem): |
225 """ A handle that can be moved by the mouse """ | 229 """ A handle that can be moved by the mouse """ |
226 def __init__(self, parent=None): | 230 def __init__(self, parent=None): |
227 super(HandleItem, self).__init__(QRectF(-4.0,-4.0,8.0,8.0), parent) | 231 dx = 12.0 |
232 super(HandleItem, self).__init__(QRectF(-0.5*dx,-0.5*dx,dx,dx), parent) | |
228 self.posChangeCallbacks = [] | 233 self.posChangeCallbacks = [] |
229 self.setBrush(QBrush(Qt.white)) | 234 self.setBrush(QBrush(Qt.white)) |
230 self.setFlag(self.ItemSendsScenePositionChanges, True) | 235 self.setFlag(self.ItemSendsScenePositionChanges, True) |
231 self.setFlag(self.ItemIsMovable, True) | 236 self.setFlag(self.ItemIsMovable, True) |
232 #self.setFlag(self.ItemIsSelectable, False) | |
233 self.setCursor(QCursor(Qt.SizeFDiagCursor)) | 237 self.setCursor(QCursor(Qt.SizeFDiagCursor)) |
234 | 238 def mouseMoveEvent(self, event): |
239 """ Move function without moving the other selected elements """ | |
240 p = self.mapToParent(event.pos()) | |
241 self.setPos(p) | |
235 def itemChange(self, change, value): | 242 def itemChange(self, change, value): |
236 if change == self.ItemPositionChange: | 243 if change == self.ItemPositionChange: |
237 x, y = value.x(), value.y() | |
238 for cb in self.posChangeCallbacks: | 244 for cb in self.posChangeCallbacks: |
239 res = cb(x, y) | 245 res = cb(value) |
240 if res: | 246 if res: |
241 x, y = res | 247 value = res |
242 value = QPointF(x, y) | |
243 return value | 248 return value |
244 # Call superclass method: | 249 # Call superclass method: |
245 return super(HandleItem, self).itemChange(change, value) | 250 return super(HandleItem, self).itemChange(change, value) |
246 | 251 |
247 class BlockItem(QGraphicsRectItem): | 252 class BlockItem(QGraphicsRectItem): |
265 self.label = QGraphicsTextItem(name, self) | 270 self.label = QGraphicsTextItem(name, self) |
266 self.name = name | 271 self.name = name |
267 # Create corner for resize: | 272 # Create corner for resize: |
268 self.sizer = HandleItem(self) | 273 self.sizer = HandleItem(self) |
269 self.sizer.posChangeCallbacks.append(self.changeSize) # Connect the callback | 274 self.sizer.posChangeCallbacks.append(self.changeSize) # Connect the callback |
270 #self.sizer.setVisible(False) | 275 self.sizer.setVisible(False) |
271 | 276 |
272 # Inputs and outputs of the block: | 277 # Inputs and outputs of the block: |
273 self.inputs = [] | 278 self.inputs = [] |
274 self.outputs = [] | 279 self.outputs = [] |
275 # Update size: | 280 # Update size: |
288 self.inputs.append(i) | 293 self.inputs.append(i) |
289 self.updateSize() | 294 self.updateSize() |
290 def addOutput(self, o): | 295 def addOutput(self, o): |
291 self.outputs.append(o) | 296 self.outputs.append(o) |
292 self.updateSize() | 297 self.updateSize() |
298 def boundingRect(self): | |
299 rect = super(BlockItem, self).boundingRect() | |
300 rect.adjust(-3, -3, 3, 3) | |
301 return rect | |
293 | 302 |
294 def contextMenuEvent(self, event): | 303 def contextMenuEvent(self, event): |
295 menu = QMenu() | 304 menu = QMenu() |
296 pa = menu.addAction('Parameters') | 305 pa = menu.addAction('Parameters') |
297 pa.triggered.connect(self.editParameters) | 306 pa.triggered.connect(self.editParameters) |
298 pa = menu.addAction('Add port') | 307 pa = menu.addAction('Add port') |
299 pa.triggered.connect(self.addPort) | 308 pa.triggered.connect(self.addPort) |
300 menu.exec_(event.screenPos()) | 309 menu.exec_(event.screenPos()) |
310 def itemChange(self, change, value): | |
311 if change == self.ItemSelectedHasChanged: | |
312 self.sizer.setVisible(value) | |
313 return super(BlockItem, self).itemChange(change, value) | |
301 | 314 |
302 def updateSize(self): | 315 def updateSize(self): |
303 rect = self.rect() | 316 rect = self.rect() |
304 h, w = rect.height(), rect.width() | 317 h, w = rect.height(), rect.width() |
305 if len(self.inputs) == 1: | 318 if len(self.inputs) == 1: |
317 dy = (h - 30) / (len(self.outputs) - 1) | 330 dy = (h - 30) / (len(self.outputs) - 1) |
318 for outp in self.outputs: | 331 for outp in self.outputs: |
319 outp.setPos(w, y) | 332 outp.setPos(w, y) |
320 y += dy | 333 y += dy |
321 | 334 |
322 def changeSize(self, w, h): | 335 def changeSize(self, p): |
323 """ Resize block function """ | 336 """ Resize block function """ |
337 w, h = p.x(), p.y() | |
324 # Limit the block size: | 338 # Limit the block size: |
325 if h < 20: | 339 if h < 20: |
326 h = 20 | 340 h = 20 |
327 if w < 40: | 341 if w < 40: |
328 w = 40 | 342 w = 40 |
333 lx = (w - lw) / 2 | 347 lx = (w - lw) / 2 |
334 ly = (h - lh) / 2 | 348 ly = (h - lh) / 2 |
335 self.label.setPos(lx, ly) | 349 self.label.setPos(lx, ly) |
336 # Update port positions: | 350 # Update port positions: |
337 self.updateSize() | 351 self.updateSize() |
338 return w, h | 352 return QPointF(w, h) |
339 | 353 |
340 class EditorGraphicsView(QGraphicsView): | 354 class EditorGraphicsView(QGraphicsView): |
341 def __init__(self, scene, parent=None): | 355 def __init__(self, scene, parent=None): |
342 QGraphicsView.__init__(self, scene, parent) | 356 QGraphicsView.__init__(self, scene, parent) |
343 self.setDragMode(QGraphicsView.RubberBandDrag) | 357 self.setDragMode(QGraphicsView.RubberBandDrag) |
358 def wheelEvent(self, event): | |
359 pos = event.pos() | |
360 posbefore = self.mapToScene(pos) | |
361 degrees = event.delta() / 8.0 | |
362 sx = (100.0 + degrees) / 100.0 | |
363 self.scale(sx, sx) | |
364 event.accept() | |
344 def dragEnterEvent(self, event): | 365 def dragEnterEvent(self, event): |
345 if event.mimeData().hasFormat('component/name'): | 366 if event.mimeData().hasFormat('component/name'): |
346 event.accept() | 367 event.accept() |
347 def dragMoveEvent(self, event): | 368 def dragMoveEvent(self, event): |
348 if event.mimeData().hasFormat('component/name'): | 369 if event.mimeData().hasFormat('component/name'): |
369 | 390 |
370 class DiagramScene(QGraphicsScene): | 391 class DiagramScene(QGraphicsScene): |
371 """ Save and load and deletion of item""" | 392 """ Save and load and deletion of item""" |
372 def __init__(self, parent=None): | 393 def __init__(self, parent=None): |
373 super(DiagramScene, self).__init__(parent) | 394 super(DiagramScene, self).__init__(parent) |
395 #circle = QGraphicsEllipseItem(-10,-10,20,20) | |
396 #self.addItem(circle) | |
397 | |
374 def saveDiagram(self, filename): | 398 def saveDiagram(self, filename): |
375 items = self.items() | 399 items = self.items() |
376 blocks = [item for item in items if type(item) is BlockItem] | 400 blocks = [item for item in items if type(item) is BlockItem] |
377 connections = [item for item in items if type(item) is Connection] | 401 connections = [item for item in items if type(item) is Connection] |
378 | 402 |
464 editor.sceneMouseMoveEvent(mouseEvent) | 488 editor.sceneMouseMoveEvent(mouseEvent) |
465 super(DiagramScene, self).mouseMoveEvent(mouseEvent) | 489 super(DiagramScene, self).mouseMoveEvent(mouseEvent) |
466 def mouseReleaseEvent(self, mouseEvent): | 490 def mouseReleaseEvent(self, mouseEvent): |
467 editor.sceneMouseReleaseEvent(mouseEvent) | 491 editor.sceneMouseReleaseEvent(mouseEvent) |
468 super(DiagramScene, self).mouseReleaseEvent(mouseEvent) | 492 super(DiagramScene, self).mouseReleaseEvent(mouseEvent) |
493 def vias(self, P1, P2): | |
494 """ Constructs a list of points that must be connected | |
495 to go from P1 to P2 """ | |
496 def stripHits(hits): | |
497 """ Helper function that removes object hits """ | |
498 hits = [hit for hit in hits if type(hit) is BlockItem] | |
499 return hits | |
500 | |
501 def viaPoints(pA, pB): | |
502 # Construct intermediate points: | |
503 | |
504 pAB1 = QPointF(pA.x(), pB.y()) | |
505 pAB2 = QPointF(pB.x(), pA.y()) | |
506 path1 = QPainterPath(pA) | |
507 path1.lineTo(pAB1) | |
508 path2 = QPainterPath(pA) | |
509 path2.lineTo(pAB2) | |
510 if len(stripHits(self.items(path1))) > len(stripHits(self.items(path2))): | |
511 pAB = pAB2 | |
512 else: | |
513 pAB = pAB1 | |
514 return [pAB] | |
515 | |
516 # Determine left or right: | |
517 dx = QPointF(20, 0) | |
518 if len(stripHits(self.items(P1 + dx))) > len(stripHits(self.items(P1 - dx))): | |
519 p2 = P1 - dx | |
520 else: | |
521 p2 = P1 + dx | |
522 | |
523 if len(stripHits(self.items(P2 + dx))) > len(stripHits(self.items(P2 - dx))): | |
524 p3 = P2 - dx | |
525 else: | |
526 p3 = P2 + dx | |
527 | |
528 # If pathitem: | |
529 pts = [P1, p2] + viaPoints(p2, p3) + [p3, P2] | |
530 return pts | |
469 def deleteItem(self, item=None): | 531 def deleteItem(self, item=None): |
470 if item: | 532 if item: |
471 if type(item) is BlockItem: | 533 if type(item) is BlockItem: |
472 for p in item.inputs + item.outputs: | 534 for p in item.inputs + item.outputs: |
473 if p.connection: | 535 if p.connection: |
542 self.diagramScene.saveDiagram('diagram2.usd') | 604 self.diagramScene.saveDiagram('diagram2.usd') |
543 def toggleFullScreen(self): | 605 def toggleFullScreen(self): |
544 self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) | 606 self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) |
545 def startConnection(self, port): | 607 def startConnection(self, port): |
546 self.startedConnection = Connection(port, None) | 608 self.startedConnection = Connection(port, None) |
609 pos = port.scenePos() | |
610 self.startedConnection.setEndPos(pos) | |
547 self.diagramScene.addItem(self.startedConnection) | 611 self.diagramScene.addItem(self.startedConnection) |
548 def zoomAll(self): | 612 def zoomAll(self): |
549 """ zoom to fit all items """ | 613 """ zoom to fit all items """ |
550 rect = self.diagramScene.itemsBoundingRect() | 614 rect = self.diagramScene.itemsBoundingRect() |
551 self.diagramView.fitInView(rect, Qt.KeepAspectRatio) | 615 self.diagramView.fitInView(rect, Qt.KeepAspectRatio) |
561 if type(item) is PortItem: | 625 if type(item) is PortItem: |
562 self.startedConnection.setToPort(item) | 626 self.startedConnection.setToPort(item) |
563 self.startedConnection = None | 627 self.startedConnection = None |
564 return | 628 return |
565 self.diagramScene.deleteItem(self.startedConnection) | 629 self.diagramScene.deleteItem(self.startedConnection) |
566 del self.startedConnection | |
567 self.startedConnection = None | 630 self.startedConnection = None |
568 | 631 |
569 if __name__ == '__main__': | 632 if __name__ == '__main__': |
570 if sys.version_info.major != 3: | 633 if sys.version_info.major != 3: |
571 print('Please use python 3.x') | 634 print('Please use python 3.x') |