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')