|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a subclass of E4GraphicsView for our diagrams. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 |
|
12 from PyQt4.QtCore import * |
|
13 from PyQt4.QtGui import * |
|
14 |
|
15 from E4Graphics.E4GraphicsView import E4GraphicsView |
|
16 |
|
17 from UMLItem import UMLItem |
|
18 from UMLSceneSizeDialog import UMLSceneSizeDialog |
|
19 from ZoomDialog import ZoomDialog |
|
20 |
|
21 import UI.Config |
|
22 import UI.PixmapCache |
|
23 |
|
24 import Preferences |
|
25 |
|
26 class UMLGraphicsView(E4GraphicsView): |
|
27 """ |
|
28 Class implementing a specialized E4GraphicsView for our diagrams. |
|
29 |
|
30 @signal relayout() emitted to indicate a relayout of the diagram |
|
31 is requested |
|
32 """ |
|
33 def __init__(self, scene, diagramName = "Unnamed", parent = None, name = None): |
|
34 """ |
|
35 Constructor |
|
36 |
|
37 @param scene reference to the scene object (QGraphicsScene) |
|
38 @param diagramName name of the diagram (string) |
|
39 @param parent parent widget of the view (QWidget) |
|
40 @param name name of the view widget (string) |
|
41 """ |
|
42 E4GraphicsView.__init__(self, scene, parent) |
|
43 if name: |
|
44 self.setObjectName(name) |
|
45 |
|
46 self.diagramName = diagramName |
|
47 |
|
48 self.border = 10 |
|
49 self.deltaSize = 100.0 |
|
50 |
|
51 self.__initActions() |
|
52 |
|
53 self.connect(scene, SIGNAL("changed(const QList<QRectF> &)"), self.__sceneChanged) |
|
54 |
|
55 def __initActions(self): |
|
56 """ |
|
57 Private method to initialize the view actions. |
|
58 """ |
|
59 self.deleteShapeAct = \ |
|
60 QAction(UI.PixmapCache.getIcon("deleteShape.png"), |
|
61 self.trUtf8("Delete shapes"), self) |
|
62 self.connect(self.deleteShapeAct, SIGNAL("triggered()"), self.__deleteShape) |
|
63 |
|
64 self.saveAct = \ |
|
65 QAction(UI.PixmapCache.getIcon("fileSave.png"), |
|
66 self.trUtf8("Save as PNG"), self) |
|
67 self.connect(self.saveAct, SIGNAL("triggered()"), self.__saveImage) |
|
68 |
|
69 self.printAct = \ |
|
70 QAction(UI.PixmapCache.getIcon("print.png"), |
|
71 self.trUtf8("Print"), self) |
|
72 self.connect(self.printAct, SIGNAL("triggered()"), self.__printDiagram) |
|
73 |
|
74 self.printPreviewAct = \ |
|
75 QAction(UI.PixmapCache.getIcon("printPreview.png"), |
|
76 self.trUtf8("Print Preview"), self) |
|
77 self.connect(self.printPreviewAct, SIGNAL("triggered()"), |
|
78 self.__printPreviewDiagram) |
|
79 |
|
80 self.zoomInAct = \ |
|
81 QAction(UI.PixmapCache.getIcon("zoomIn.png"), |
|
82 self.trUtf8("Zoom in"), self) |
|
83 self.connect(self.zoomInAct, SIGNAL("triggered()"), self.zoomIn) |
|
84 |
|
85 self.zoomOutAct = \ |
|
86 QAction(UI.PixmapCache.getIcon("zoomOut.png"), |
|
87 self.trUtf8("Zoom out"), self) |
|
88 self.connect(self.zoomOutAct, SIGNAL("triggered()"), self.zoomOut) |
|
89 |
|
90 self.zoomAct = \ |
|
91 QAction(UI.PixmapCache.getIcon("zoomTo.png"), |
|
92 self.trUtf8("Zoom..."), self) |
|
93 self.connect(self.zoomAct, SIGNAL("triggered()"), self.__zoom) |
|
94 |
|
95 self.zoomResetAct = \ |
|
96 QAction(UI.PixmapCache.getIcon("zoomReset.png"), |
|
97 self.trUtf8("Zoom reset"), self) |
|
98 self.connect(self.zoomResetAct, SIGNAL("triggered()"), self.zoomReset) |
|
99 |
|
100 self.incWidthAct = \ |
|
101 QAction(UI.PixmapCache.getIcon("sceneWidthInc.png"), |
|
102 self.trUtf8("Increase width by {0} points").format(self.deltaSize), |
|
103 self) |
|
104 self.connect(self.incWidthAct, SIGNAL("triggered()"), self.__incWidth) |
|
105 |
|
106 self.incHeightAct = \ |
|
107 QAction(UI.PixmapCache.getIcon("sceneHeightInc.png"), |
|
108 self.trUtf8("Increase height by {0} points").format(self.deltaSize), |
|
109 self) |
|
110 self.connect(self.incHeightAct, SIGNAL("triggered()"), self.__incHeight) |
|
111 |
|
112 self.decWidthAct = \ |
|
113 QAction(UI.PixmapCache.getIcon("sceneWidthDec.png"), |
|
114 self.trUtf8("Decrease width by {0} points").format(self.deltaSize), |
|
115 self) |
|
116 self.connect(self.decWidthAct, SIGNAL("triggered()"), self.__decWidth) |
|
117 |
|
118 self.decHeightAct = \ |
|
119 QAction(UI.PixmapCache.getIcon("sceneHeightDec.png"), |
|
120 self.trUtf8("Decrease height by {0} points").format(self.deltaSize), |
|
121 self) |
|
122 self.connect(self.decHeightAct, SIGNAL("triggered()"), self.__decHeight) |
|
123 |
|
124 self.setSizeAct = \ |
|
125 QAction(UI.PixmapCache.getIcon("sceneSize.png"), |
|
126 self.trUtf8("Set size"), self) |
|
127 self.connect(self.setSizeAct, SIGNAL("triggered()"), self.__setSize) |
|
128 |
|
129 self.relayoutAct = \ |
|
130 QAction(UI.PixmapCache.getIcon("reload.png"), |
|
131 self.trUtf8("Re-Layout"), self) |
|
132 self.connect(self.relayoutAct, SIGNAL("triggered()"), self.__relayout) |
|
133 |
|
134 self.alignLeftAct = \ |
|
135 QAction(UI.PixmapCache.getIcon("shapesAlignLeft"), |
|
136 self.trUtf8("Align Left"), self) |
|
137 self.connect(self.alignLeftAct, SIGNAL("triggered()"), |
|
138 lambda align=Qt.AlignLeft: self.__alignShapes(align)) |
|
139 |
|
140 self.alignHCenterAct = \ |
|
141 QAction(UI.PixmapCache.getIcon("shapesAlignHCenter"), |
|
142 self.trUtf8("Align Center Horizontal"), self) |
|
143 self.connect(self.alignHCenterAct, SIGNAL("triggered()"), |
|
144 lambda align=Qt.AlignHCenter: self.__alignShapes(align)) |
|
145 |
|
146 self.alignRightAct = \ |
|
147 QAction(UI.PixmapCache.getIcon("shapesAlignRight"), |
|
148 self.trUtf8("Align Right"), self) |
|
149 self.connect(self.alignRightAct, SIGNAL("triggered()"), |
|
150 lambda align=Qt.AlignRight: self.__alignShapes(align)) |
|
151 |
|
152 self.alignTopAct = \ |
|
153 QAction(UI.PixmapCache.getIcon("shapesAlignTop"), |
|
154 self.trUtf8("Align Top"), self) |
|
155 self.connect(self.alignTopAct, SIGNAL("triggered()"), |
|
156 lambda align=Qt.AlignTop: self.__alignShapes(align)) |
|
157 |
|
158 self.alignVCenterAct = \ |
|
159 QAction(UI.PixmapCache.getIcon("shapesAlignVCenter"), |
|
160 self.trUtf8("Align Center Vertical"), self) |
|
161 self.connect(self.alignVCenterAct, SIGNAL("triggered()"), |
|
162 lambda align=Qt.AlignVCenter: self.__alignShapes(align)) |
|
163 |
|
164 self.alignBottomAct = \ |
|
165 QAction(UI.PixmapCache.getIcon("shapesAlignBottom"), |
|
166 self.trUtf8("Align Bottom"), self) |
|
167 self.connect(self.alignBottomAct, SIGNAL("triggered()"), |
|
168 lambda align=Qt.AlignBottom: self.__alignShapes(align)) |
|
169 |
|
170 def __checkSizeActions(self): |
|
171 """ |
|
172 Private slot to set the enabled state of the size actions. |
|
173 """ |
|
174 diagramSize = self._getDiagramSize(10) |
|
175 sceneRect = self.scene().sceneRect() |
|
176 if (sceneRect.width() - self.deltaSize) <= diagramSize.width(): |
|
177 self.decWidthAct.setEnabled(False) |
|
178 else: |
|
179 self.decWidthAct.setEnabled(True) |
|
180 if (sceneRect.height() - self.deltaSize) <= diagramSize.height(): |
|
181 self.decHeightAct.setEnabled(False) |
|
182 else: |
|
183 self.decHeightAct.setEnabled(True) |
|
184 |
|
185 def __sceneChanged(self, areas): |
|
186 """ |
|
187 Private slot called when the scene changes. |
|
188 |
|
189 @param areas list of rectangles that contain changes (list of QRectF) |
|
190 """ |
|
191 if len(self.scene().selectedItems()) > 0: |
|
192 self.deleteShapeAct.setEnabled(True) |
|
193 else: |
|
194 self.deleteShapeAct.setEnabled(False) |
|
195 |
|
196 def initToolBar(self): |
|
197 """ |
|
198 Public method to populate a toolbar with our actions. |
|
199 |
|
200 @return the populated toolBar (QToolBar) |
|
201 """ |
|
202 toolBar = QToolBar(self.trUtf8("Graphics"), self) |
|
203 toolBar.setIconSize(UI.Config.ToolBarIconSize) |
|
204 toolBar.addAction(self.deleteShapeAct) |
|
205 toolBar.addSeparator() |
|
206 toolBar.addAction(self.saveAct) |
|
207 toolBar.addSeparator() |
|
208 toolBar.addAction(self.printPreviewAct) |
|
209 toolBar.addAction(self.printAct) |
|
210 toolBar.addSeparator() |
|
211 toolBar.addAction(self.zoomInAct) |
|
212 toolBar.addAction(self.zoomOutAct) |
|
213 toolBar.addAction(self.zoomAct) |
|
214 toolBar.addAction(self.zoomResetAct) |
|
215 toolBar.addSeparator() |
|
216 toolBar.addAction(self.alignLeftAct) |
|
217 toolBar.addAction(self.alignHCenterAct) |
|
218 toolBar.addAction(self.alignRightAct) |
|
219 toolBar.addAction(self.alignTopAct) |
|
220 toolBar.addAction(self.alignVCenterAct) |
|
221 toolBar.addAction(self.alignBottomAct) |
|
222 toolBar.addSeparator() |
|
223 toolBar.addAction(self.incWidthAct) |
|
224 toolBar.addAction(self.incHeightAct) |
|
225 toolBar.addAction(self.decWidthAct) |
|
226 toolBar.addAction(self.decHeightAct) |
|
227 toolBar.addAction(self.setSizeAct) |
|
228 toolBar.addSeparator() |
|
229 toolBar.addAction(self.relayoutAct) |
|
230 |
|
231 return toolBar |
|
232 |
|
233 def filteredItems(self, items): |
|
234 """ |
|
235 Public method to filter a list of items. |
|
236 |
|
237 @param items list of items as returned by the scene object |
|
238 (QGraphicsItem) |
|
239 @return list of interesting collision items (QGraphicsItem) |
|
240 """ |
|
241 return [itm for itm in items if isinstance(itm, UMLItem)] |
|
242 |
|
243 def selectItems(self, items): |
|
244 """ |
|
245 Public method to select the given items. |
|
246 |
|
247 @param items list of items to be selected (list of QGraphicsItemItem) |
|
248 """ |
|
249 # step 1: deselect all items |
|
250 self.unselectItems() |
|
251 |
|
252 # step 2: select all given items |
|
253 for itm in items: |
|
254 if isinstance(itm, UMLWidget): |
|
255 itm.setSelected(True) |
|
256 |
|
257 def selectItem(self, item): |
|
258 """ |
|
259 Public method to select an item. |
|
260 |
|
261 @param item item to be selected (QGraphicsItemItem) |
|
262 """ |
|
263 if isinstance(item, UMLWidget): |
|
264 item.setSelected(not item.isSelected()) |
|
265 |
|
266 def __deleteShape(self): |
|
267 """ |
|
268 Private method to delete the selected shapes from the display. |
|
269 """ |
|
270 for item in self.scene().selectedItems(): |
|
271 item.removeAssociations() |
|
272 item.setSelected(False) |
|
273 self.scene().removeItem(item) |
|
274 del item |
|
275 |
|
276 def __incWidth(self): |
|
277 """ |
|
278 Private method to handle the increase width context menu entry. |
|
279 """ |
|
280 self.resizeScene(self.deltaSize, True) |
|
281 self.__checkSizeActions() |
|
282 |
|
283 def __incHeight(self): |
|
284 """ |
|
285 Private method to handle the increase height context menu entry. |
|
286 """ |
|
287 self.resizeScene(self.deltaSize, False) |
|
288 self.__checkSizeActions() |
|
289 |
|
290 def __decWidth(self): |
|
291 """ |
|
292 Private method to handle the decrease width context menu entry. |
|
293 """ |
|
294 self.resizeScene(-self.deltaSize, True) |
|
295 self.__checkSizeActions() |
|
296 |
|
297 def __decHeight(self): |
|
298 """ |
|
299 Private method to handle the decrease height context menu entry. |
|
300 """ |
|
301 self.resizeScene(-self.deltaSize, False) |
|
302 self.__checkSizeActions() |
|
303 |
|
304 def __setSize(self): |
|
305 """ |
|
306 Private method to handle the set size context menu entry. |
|
307 """ |
|
308 rect = self._getDiagramRect(10) |
|
309 sceneRect = self.scene().sceneRect() |
|
310 dlg = UMLSceneSizeDialog(sceneRect.width(), sceneRect.height(), |
|
311 rect.width(), rect.height(), self) |
|
312 if dlg.exec_() == QDialog.Accepted: |
|
313 width, height = dlg.getData() |
|
314 self.setSceneSize(width, height) |
|
315 self.__checkSizeActions() |
|
316 |
|
317 def __saveImage(self): |
|
318 """ |
|
319 Private method to handle the save context menu entry. |
|
320 """ |
|
321 fname, selectedFilter = QFileDialog.getSaveFileNameAndFilter(\ |
|
322 self, |
|
323 self.trUtf8("Save Diagram"), |
|
324 "", |
|
325 self.trUtf8("Portable Network Graphics (*.png);;" |
|
326 "Scalable Vector Graphics (*.svg)"), |
|
327 "", |
|
328 QFileDialog.Options(QFileDialog.DontConfirmOverwrite)) |
|
329 if fname: |
|
330 ext = QFileInfo(fname).suffix() |
|
331 if not ext: |
|
332 ex = selectedFilter.split("(*")[1].split(")")[0] |
|
333 if ex: |
|
334 fname += ex |
|
335 if QFileInfo(fname).exists(): |
|
336 res = QMessageBox.warning(self, |
|
337 self.trUtf8("Save Diagram"), |
|
338 self.trUtf8("<p>The file <b>{0}</b> already exists.</p>") |
|
339 .format(fname), |
|
340 QMessageBox.StandardButtons(\ |
|
341 QMessageBox.Abort | \ |
|
342 QMessageBox.Save), |
|
343 QMessageBox.Abort) |
|
344 if res == QMessageBox.Abort or res == QMessageBox.Cancel: |
|
345 return |
|
346 |
|
347 success = self.saveImage(fname, QFileInfo(fname).suffix().upper()) |
|
348 if not success: |
|
349 QMessageBox.critical(None, |
|
350 self.trUtf8("Save Diagram"), |
|
351 self.trUtf8("""<p>The file <b>{0}</b> could not be saved.</p>""") |
|
352 .format(fname)) |
|
353 |
|
354 def __relayout(self): |
|
355 """ |
|
356 Private method to handle the re-layout context menu entry. |
|
357 """ |
|
358 scene = self.scene() |
|
359 for itm in scene.items()[:]: |
|
360 if itm.scene() == scene: |
|
361 scene.removeItem(itm) |
|
362 self.emit(SIGNAL("relayout()")) |
|
363 |
|
364 def __printDiagram(self): |
|
365 """ |
|
366 Private slot called to print the diagram. |
|
367 """ |
|
368 printer = QPrinter(mode = QPrinter.ScreenResolution) |
|
369 printer.setFullPage(True) |
|
370 if Preferences.getPrinter("ColorMode"): |
|
371 printer.setColorMode(QPrinter.Color) |
|
372 else: |
|
373 printer.setColorMode(QPrinter.GrayScale) |
|
374 if Preferences.getPrinter("FirstPageFirst"): |
|
375 printer.setPageOrder(QPrinter.FirstPageFirst) |
|
376 else: |
|
377 printer.setPageOrder(QPrinter.LastPageFirst) |
|
378 printer.setPrinterName(Preferences.getPrinter("PrinterName")) |
|
379 |
|
380 printDialog = QPrintDialog(printer, self) |
|
381 if printDialog.exec_(): |
|
382 self.printDiagram(printer, self.diagramName) |
|
383 |
|
384 def __printPreviewDiagram(self): |
|
385 """ |
|
386 Private slot called to show a print preview of the diagram. |
|
387 """ |
|
388 from PyQt4.QtGui import QPrintPreviewDialog |
|
389 |
|
390 printer = QPrinter(mode = QPrinter.ScreenResolution) |
|
391 printer.setFullPage(True) |
|
392 if Preferences.getPrinter("ColorMode"): |
|
393 printer.setColorMode(QPrinter.Color) |
|
394 else: |
|
395 printer.setColorMode(QPrinter.GrayScale) |
|
396 if Preferences.getPrinter("FirstPageFirst"): |
|
397 printer.setPageOrder(QPrinter.FirstPageFirst) |
|
398 else: |
|
399 printer.setPageOrder(QPrinter.LastPageFirst) |
|
400 printer.setPrinterName(Preferences.getPrinter("PrinterName")) |
|
401 |
|
402 preview = QPrintPreviewDialog(printer, self) |
|
403 self.connect(preview, SIGNAL("paintRequested(QPrinter*)"), self.printDiagram) |
|
404 preview.exec_() |
|
405 |
|
406 def __zoom(self): |
|
407 """ |
|
408 Private method to handle the zoom context menu action. |
|
409 """ |
|
410 dlg = ZoomDialog(self.zoom(), self) |
|
411 if dlg.exec_() == QDialog.Accepted: |
|
412 zoom = dlg.getZoomSize() |
|
413 self.setZoom(zoom) |
|
414 |
|
415 def setDiagramName(self, name): |
|
416 """ |
|
417 Public slot to set the diagram name. |
|
418 |
|
419 @param name diagram name (string) |
|
420 """ |
|
421 self.diagramName = name |
|
422 |
|
423 def __alignShapes(self, alignment): |
|
424 """ |
|
425 Private slot to align the selected shapes. |
|
426 |
|
427 @param alignment alignment type (Qt.AlignmentFlag) |
|
428 """ |
|
429 # step 1: get all selected items |
|
430 items = self.scene().selectedItems() |
|
431 if len(items) <= 1: |
|
432 return |
|
433 |
|
434 # step 2: find the index of the item to align in relation to |
|
435 amount = None |
|
436 for i, item in enumerate(items): |
|
437 rect = item.sceneBoundingRect() |
|
438 if alignment == Qt.AlignLeft: |
|
439 if amount is None or rect.x() < amount: |
|
440 amount = rect.x() |
|
441 index = i |
|
442 elif alignment == Qt.AlignRight: |
|
443 if amount is None or rect.x() + rect.width() > amount: |
|
444 amount = rect.x() + rect.width() |
|
445 index = i |
|
446 elif alignment == Qt.AlignHCenter: |
|
447 if amount is None or rect.width() > amount: |
|
448 amount = rect.width() |
|
449 index = i |
|
450 elif alignment == Qt.AlignTop: |
|
451 if amount is None or rect.y() < amount: |
|
452 amount = rect.y() |
|
453 index = i |
|
454 elif alignment == Qt.AlignBottom: |
|
455 if amount is None or rect.y() + rect.height() > amount: |
|
456 amount = rect.y() + rect.height() |
|
457 index = i |
|
458 elif alignment == Qt.AlignVCenter: |
|
459 if amount is None or rect.height() > amount: |
|
460 amount = rect.height() |
|
461 index = i |
|
462 rect = items[index].sceneBoundingRect() |
|
463 |
|
464 # step 3: move the other items |
|
465 for i, item in enumerate(items): |
|
466 if i == index: |
|
467 continue |
|
468 itemrect = item.sceneBoundingRect() |
|
469 xOffset = yOffset = 0 |
|
470 if alignment == Qt.AlignLeft: |
|
471 xOffset = rect.x() - itemrect.x() |
|
472 elif alignment == Qt.AlignRight: |
|
473 xOffset = (rect.x() + rect.width()) - \ |
|
474 (itemrect.x() + itemrect.width()) |
|
475 elif alignment == Qt.AlignHCenter: |
|
476 xOffset = (rect.x() + rect.width() / 2) - \ |
|
477 (itemrect.x() + itemrect.width() / 2) |
|
478 elif alignment == Qt.AlignTop: |
|
479 yOffset = rect.y() - itemrect.y() |
|
480 elif alignment == Qt.AlignBottom: |
|
481 yOffset = (rect.y() + rect.height()) - \ |
|
482 (itemrect.y() + itemrect.height()) |
|
483 elif alignment == Qt.AlignVCenter: |
|
484 yOffset = (rect.y() + rect.height() / 2) - \ |
|
485 (itemrect.y() + itemrect.height() / 2) |
|
486 item.moveBy(xOffset, yOffset) |
|
487 |
|
488 self.scene().update() |