|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a subclass of E5GraphicsView for our diagrams. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 from PyQt5.QtCore import pyqtSignal, Qt, QSignalMapper, QFileInfo, QEvent, \ |
|
13 QRectF |
|
14 from PyQt5.QtWidgets import QGraphicsView, QAction, QToolBar, QDialog |
|
15 from PyQt5.QtPrintSupport import QPrinter, QPrintDialog |
|
16 |
|
17 from E5Graphics.E5GraphicsView import E5GraphicsView |
|
18 |
|
19 from E5Gui import E5MessageBox, E5FileDialog |
|
20 from E5Gui.E5ZoomWidget import E5ZoomWidget |
|
21 |
|
22 from .UMLItem import UMLItem |
|
23 |
|
24 import UI.Config |
|
25 import UI.PixmapCache |
|
26 |
|
27 import Preferences |
|
28 from Globals import qVersionTuple |
|
29 |
|
30 |
|
31 class UMLGraphicsView(E5GraphicsView): |
|
32 """ |
|
33 Class implementing a specialized E5GraphicsView for our diagrams. |
|
34 |
|
35 @signal relayout() emitted to indicate a relayout of the diagram |
|
36 is requested |
|
37 """ |
|
38 relayout = pyqtSignal() |
|
39 |
|
40 def __init__(self, scene, parent=None): |
|
41 """ |
|
42 Constructor |
|
43 |
|
44 @param scene reference to the scene object (QGraphicsScene) |
|
45 @param parent parent widget of the view (QWidget) |
|
46 """ |
|
47 E5GraphicsView.__init__(self, scene, parent) |
|
48 self.setObjectName("UMLGraphicsView") |
|
49 self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) |
|
50 |
|
51 self.diagramName = "Unnamed" |
|
52 self.__itemId = -1 |
|
53 |
|
54 self.border = 10 |
|
55 self.deltaSize = 100.0 |
|
56 |
|
57 self.__zoomWidget = E5ZoomWidget( |
|
58 UI.PixmapCache.getPixmap("zoomOut.png"), |
|
59 UI.PixmapCache.getPixmap("zoomIn.png"), |
|
60 UI.PixmapCache.getPixmap("zoomReset.png"), self) |
|
61 parent.statusBar().addPermanentWidget(self.__zoomWidget) |
|
62 self.__zoomWidget.setMapping( |
|
63 E5GraphicsView.ZoomLevels, E5GraphicsView.ZoomLevelDefault) |
|
64 self.__zoomWidget.valueChanged.connect(self.setZoom) |
|
65 self.zoomValueChanged.connect(self.__zoomWidget.setValue) |
|
66 |
|
67 self.__initActions() |
|
68 |
|
69 scene.changed.connect(self.__sceneChanged) |
|
70 |
|
71 self.grabGesture(Qt.PinchGesture) |
|
72 |
|
73 def __initActions(self): |
|
74 """ |
|
75 Private method to initialize the view actions. |
|
76 """ |
|
77 self.alignMapper = QSignalMapper(self) |
|
78 self.alignMapper.mapped[int].connect(self.__alignShapes) |
|
79 |
|
80 self.deleteShapeAct = \ |
|
81 QAction(UI.PixmapCache.getIcon("deleteShape.png"), |
|
82 self.tr("Delete shapes"), self) |
|
83 self.deleteShapeAct.triggered.connect(self.__deleteShape) |
|
84 |
|
85 self.incWidthAct = \ |
|
86 QAction(UI.PixmapCache.getIcon("sceneWidthInc.png"), |
|
87 self.tr("Increase width by {0} points").format( |
|
88 self.deltaSize), |
|
89 self) |
|
90 self.incWidthAct.triggered.connect(self.__incWidth) |
|
91 |
|
92 self.incHeightAct = \ |
|
93 QAction(UI.PixmapCache.getIcon("sceneHeightInc.png"), |
|
94 self.tr("Increase height by {0} points").format( |
|
95 self.deltaSize), |
|
96 self) |
|
97 self.incHeightAct.triggered.connect(self.__incHeight) |
|
98 |
|
99 self.decWidthAct = \ |
|
100 QAction(UI.PixmapCache.getIcon("sceneWidthDec.png"), |
|
101 self.tr("Decrease width by {0} points").format( |
|
102 self.deltaSize), |
|
103 self) |
|
104 self.decWidthAct.triggered.connect(self.__decWidth) |
|
105 |
|
106 self.decHeightAct = \ |
|
107 QAction(UI.PixmapCache.getIcon("sceneHeightDec.png"), |
|
108 self.tr("Decrease height by {0} points").format( |
|
109 self.deltaSize), |
|
110 self) |
|
111 self.decHeightAct.triggered.connect(self.__decHeight) |
|
112 |
|
113 self.setSizeAct = \ |
|
114 QAction(UI.PixmapCache.getIcon("sceneSize.png"), |
|
115 self.tr("Set size"), self) |
|
116 self.setSizeAct.triggered.connect(self.__setSize) |
|
117 |
|
118 self.rescanAct = \ |
|
119 QAction(UI.PixmapCache.getIcon("rescan.png"), |
|
120 self.tr("Re-Scan"), self) |
|
121 self.rescanAct.triggered.connect(self.__rescan) |
|
122 |
|
123 self.relayoutAct = \ |
|
124 QAction(UI.PixmapCache.getIcon("relayout.png"), |
|
125 self.tr("Re-Layout"), self) |
|
126 self.relayoutAct.triggered.connect(self.__relayout) |
|
127 |
|
128 self.alignLeftAct = \ |
|
129 QAction(UI.PixmapCache.getIcon("shapesAlignLeft.png"), |
|
130 self.tr("Align Left"), self) |
|
131 self.alignMapper.setMapping(self.alignLeftAct, Qt.AlignLeft) |
|
132 self.alignLeftAct.triggered.connect(self.alignMapper.map) |
|
133 |
|
134 self.alignHCenterAct = \ |
|
135 QAction(UI.PixmapCache.getIcon("shapesAlignHCenter.png"), |
|
136 self.tr("Align Center Horizontal"), self) |
|
137 self.alignMapper.setMapping(self.alignHCenterAct, Qt.AlignHCenter) |
|
138 self.alignHCenterAct.triggered.connect(self.alignMapper.map) |
|
139 |
|
140 self.alignRightAct = \ |
|
141 QAction(UI.PixmapCache.getIcon("shapesAlignRight.png"), |
|
142 self.tr("Align Right"), self) |
|
143 self.alignMapper.setMapping(self.alignRightAct, Qt.AlignRight) |
|
144 self.alignRightAct.triggered.connect(self.alignMapper.map) |
|
145 |
|
146 self.alignTopAct = \ |
|
147 QAction(UI.PixmapCache.getIcon("shapesAlignTop.png"), |
|
148 self.tr("Align Top"), self) |
|
149 self.alignMapper.setMapping(self.alignTopAct, Qt.AlignTop) |
|
150 self.alignTopAct.triggered.connect(self.alignMapper.map) |
|
151 |
|
152 self.alignVCenterAct = \ |
|
153 QAction(UI.PixmapCache.getIcon("shapesAlignVCenter.png"), |
|
154 self.tr("Align Center Vertical"), self) |
|
155 self.alignMapper.setMapping(self.alignVCenterAct, Qt.AlignVCenter) |
|
156 self.alignVCenterAct.triggered.connect(self.alignMapper.map) |
|
157 |
|
158 self.alignBottomAct = \ |
|
159 QAction(UI.PixmapCache.getIcon("shapesAlignBottom.png"), |
|
160 self.tr("Align Bottom"), self) |
|
161 self.alignMapper.setMapping(self.alignBottomAct, Qt.AlignBottom) |
|
162 self.alignBottomAct.triggered.connect(self.alignMapper.map) |
|
163 |
|
164 def __checkSizeActions(self): |
|
165 """ |
|
166 Private slot to set the enabled state of the size actions. |
|
167 """ |
|
168 diagramSize = self._getDiagramSize(10) |
|
169 sceneRect = self.scene().sceneRect() |
|
170 if (sceneRect.width() - self.deltaSize) < diagramSize.width(): |
|
171 self.decWidthAct.setEnabled(False) |
|
172 else: |
|
173 self.decWidthAct.setEnabled(True) |
|
174 if (sceneRect.height() - self.deltaSize) < diagramSize.height(): |
|
175 self.decHeightAct.setEnabled(False) |
|
176 else: |
|
177 self.decHeightAct.setEnabled(True) |
|
178 |
|
179 def __sceneChanged(self, areas): |
|
180 """ |
|
181 Private slot called when the scene changes. |
|
182 |
|
183 @param areas list of rectangles that contain changes (list of QRectF) |
|
184 """ |
|
185 if len(self.scene().selectedItems()) > 0: |
|
186 self.deleteShapeAct.setEnabled(True) |
|
187 else: |
|
188 self.deleteShapeAct.setEnabled(False) |
|
189 |
|
190 sceneRect = self.scene().sceneRect() |
|
191 newWidth = width = sceneRect.width() |
|
192 newHeight = height = sceneRect.height() |
|
193 rect = self.scene().itemsBoundingRect() |
|
194 # calculate with 10 pixel border on each side |
|
195 if sceneRect.right() - 10 < rect.right(): |
|
196 newWidth = rect.right() + 10 |
|
197 if sceneRect.bottom() - 10 < rect.bottom(): |
|
198 newHeight = rect.bottom() + 10 |
|
199 |
|
200 if newHeight != height or newWidth != width: |
|
201 self.setSceneSize(newWidth, newHeight) |
|
202 self.__checkSizeActions() |
|
203 |
|
204 def initToolBar(self): |
|
205 """ |
|
206 Public method to populate a toolbar with our actions. |
|
207 |
|
208 @return the populated toolBar (QToolBar) |
|
209 """ |
|
210 toolBar = QToolBar(self.tr("Graphics"), self) |
|
211 toolBar.setIconSize(UI.Config.ToolBarIconSize) |
|
212 toolBar.addAction(self.deleteShapeAct) |
|
213 toolBar.addSeparator() |
|
214 toolBar.addAction(self.alignLeftAct) |
|
215 toolBar.addAction(self.alignHCenterAct) |
|
216 toolBar.addAction(self.alignRightAct) |
|
217 toolBar.addAction(self.alignTopAct) |
|
218 toolBar.addAction(self.alignVCenterAct) |
|
219 toolBar.addAction(self.alignBottomAct) |
|
220 toolBar.addSeparator() |
|
221 toolBar.addAction(self.incWidthAct) |
|
222 toolBar.addAction(self.incHeightAct) |
|
223 toolBar.addAction(self.decWidthAct) |
|
224 toolBar.addAction(self.decHeightAct) |
|
225 toolBar.addAction(self.setSizeAct) |
|
226 toolBar.addSeparator() |
|
227 toolBar.addAction(self.rescanAct) |
|
228 toolBar.addAction(self.relayoutAct) |
|
229 |
|
230 return toolBar |
|
231 |
|
232 def filteredItems(self, items, itemType=UMLItem): |
|
233 """ |
|
234 Public method to filter a list of items. |
|
235 |
|
236 @param items list of items as returned by the scene object |
|
237 (QGraphicsItem) |
|
238 @param itemType type to be filtered (class) |
|
239 @return list of interesting collision items (QGraphicsItem) |
|
240 """ |
|
241 return [itm for itm in items if isinstance(itm, itemType)] |
|
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, UMLItem): |
|
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, UMLItem): |
|
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 from .UMLSceneSizeDialog import UMLSceneSizeDialog |
|
309 rect = self._getDiagramRect(10) |
|
310 sceneRect = self.scene().sceneRect() |
|
311 dlg = UMLSceneSizeDialog(sceneRect.width(), sceneRect.height(), |
|
312 rect.width(), rect.height(), self) |
|
313 if dlg.exec_() == QDialog.Accepted: |
|
314 width, height = dlg.getData() |
|
315 self.setSceneSize(width, height) |
|
316 self.__checkSizeActions() |
|
317 |
|
318 def autoAdjustSceneSize(self, limit=False): |
|
319 """ |
|
320 Public method to adjust the scene size to the diagram size. |
|
321 |
|
322 @param limit flag indicating to limit the scene to the |
|
323 initial size (boolean) |
|
324 """ |
|
325 super(UMLGraphicsView, self).autoAdjustSceneSize(limit=limit) |
|
326 self.__checkSizeActions() |
|
327 |
|
328 def saveImage(self): |
|
329 """ |
|
330 Public method to handle the save context menu entry. |
|
331 """ |
|
332 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( |
|
333 self, |
|
334 self.tr("Save Diagram"), |
|
335 "", |
|
336 self.tr("Portable Network Graphics (*.png);;" |
|
337 "Scalable Vector Graphics (*.svg)"), |
|
338 "", |
|
339 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) |
|
340 if fname: |
|
341 ext = QFileInfo(fname).suffix() |
|
342 if not ext: |
|
343 ex = selectedFilter.split("(*")[1].split(")")[0] |
|
344 if ex: |
|
345 fname += ex |
|
346 if QFileInfo(fname).exists(): |
|
347 res = E5MessageBox.yesNo( |
|
348 self, |
|
349 self.tr("Save Diagram"), |
|
350 self.tr("<p>The file <b>{0}</b> already exists." |
|
351 " Overwrite it?</p>").format(fname), |
|
352 icon=E5MessageBox.Warning) |
|
353 if not res: |
|
354 return |
|
355 |
|
356 success = super(UMLGraphicsView, self).saveImage( |
|
357 fname, QFileInfo(fname).suffix().upper()) |
|
358 if not success: |
|
359 E5MessageBox.critical( |
|
360 self, |
|
361 self.tr("Save Diagram"), |
|
362 self.tr( |
|
363 """<p>The file <b>{0}</b> could not be saved.</p>""") |
|
364 .format(fname)) |
|
365 |
|
366 def __relayout(self): |
|
367 """ |
|
368 Private slot to handle the re-layout context menu entry. |
|
369 """ |
|
370 self.__itemId = -1 |
|
371 self.scene().clear() |
|
372 self.relayout.emit() |
|
373 |
|
374 def __rescan(self): |
|
375 """ |
|
376 Private slot to handle the re-scan context menu entry. |
|
377 """ |
|
378 # 1. save positions of all items and names of selected items |
|
379 itemPositions = {} |
|
380 selectedItems = [] |
|
381 for item in self.filteredItems(self.scene().items(), UMLItem): |
|
382 name = item.getName() |
|
383 if name: |
|
384 itemPositions[name] = (item.x(), item.y()) |
|
385 if item.isSelected(): |
|
386 selectedItems.append(name) |
|
387 |
|
388 # 2. save |
|
389 |
|
390 # 2. re-layout the diagram |
|
391 self.__relayout() |
|
392 |
|
393 # 3. move known items to the saved positions |
|
394 for item in self.filteredItems(self.scene().items(), UMLItem): |
|
395 name = item.getName() |
|
396 if name in itemPositions: |
|
397 item.setPos(*itemPositions[name]) |
|
398 if name in selectedItems: |
|
399 item.setSelected(True) |
|
400 |
|
401 def printDiagram(self): |
|
402 """ |
|
403 Public slot called to print the diagram. |
|
404 """ |
|
405 printer = QPrinter(mode=QPrinter.ScreenResolution) |
|
406 printer.setFullPage(True) |
|
407 if Preferences.getPrinter("ColorMode"): |
|
408 printer.setColorMode(QPrinter.Color) |
|
409 else: |
|
410 printer.setColorMode(QPrinter.GrayScale) |
|
411 if Preferences.getPrinter("FirstPageFirst"): |
|
412 printer.setPageOrder(QPrinter.FirstPageFirst) |
|
413 else: |
|
414 printer.setPageOrder(QPrinter.LastPageFirst) |
|
415 printer.setPageMargins( |
|
416 Preferences.getPrinter("LeftMargin") * 10, |
|
417 Preferences.getPrinter("TopMargin") * 10, |
|
418 Preferences.getPrinter("RightMargin") * 10, |
|
419 Preferences.getPrinter("BottomMargin") * 10, |
|
420 QPrinter.Millimeter |
|
421 ) |
|
422 printerName = Preferences.getPrinter("PrinterName") |
|
423 if printerName: |
|
424 printer.setPrinterName(printerName) |
|
425 |
|
426 printDialog = QPrintDialog(printer, self) |
|
427 if printDialog.exec_(): |
|
428 super(UMLGraphicsView, self).printDiagram( |
|
429 printer, self.diagramName) |
|
430 |
|
431 def printPreviewDiagram(self): |
|
432 """ |
|
433 Public slot called to show a print preview of the diagram. |
|
434 """ |
|
435 from PyQt5.QtPrintSupport import QPrintPreviewDialog |
|
436 |
|
437 printer = QPrinter(mode=QPrinter.ScreenResolution) |
|
438 printer.setFullPage(True) |
|
439 if Preferences.getPrinter("ColorMode"): |
|
440 printer.setColorMode(QPrinter.Color) |
|
441 else: |
|
442 printer.setColorMode(QPrinter.GrayScale) |
|
443 if Preferences.getPrinter("FirstPageFirst"): |
|
444 printer.setPageOrder(QPrinter.FirstPageFirst) |
|
445 else: |
|
446 printer.setPageOrder(QPrinter.LastPageFirst) |
|
447 printer.setPageMargins( |
|
448 Preferences.getPrinter("LeftMargin") * 10, |
|
449 Preferences.getPrinter("TopMargin") * 10, |
|
450 Preferences.getPrinter("RightMargin") * 10, |
|
451 Preferences.getPrinter("BottomMargin") * 10, |
|
452 QPrinter.Millimeter |
|
453 ) |
|
454 printerName = Preferences.getPrinter("PrinterName") |
|
455 if printerName: |
|
456 printer.setPrinterName(printerName) |
|
457 |
|
458 preview = QPrintPreviewDialog(printer, self) |
|
459 preview.paintRequested[QPrinter].connect(self.__printPreviewPrint) |
|
460 preview.exec_() |
|
461 |
|
462 def __printPreviewPrint(self, printer): |
|
463 """ |
|
464 Private slot to generate a print preview. |
|
465 |
|
466 @param printer reference to the printer object (QPrinter) |
|
467 """ |
|
468 super(UMLGraphicsView, self).printDiagram(printer, self.diagramName) |
|
469 |
|
470 def setDiagramName(self, name): |
|
471 """ |
|
472 Public slot to set the diagram name. |
|
473 |
|
474 @param name diagram name (string) |
|
475 """ |
|
476 self.diagramName = name |
|
477 |
|
478 def __alignShapes(self, alignment): |
|
479 """ |
|
480 Private slot to align the selected shapes. |
|
481 |
|
482 @param alignment alignment type (Qt.AlignmentFlag) |
|
483 """ |
|
484 # step 1: get all selected items |
|
485 items = self.scene().selectedItems() |
|
486 if len(items) <= 1: |
|
487 return |
|
488 |
|
489 # step 2: find the index of the item to align in relation to |
|
490 amount = None |
|
491 for i, item in enumerate(items): |
|
492 rect = item.sceneBoundingRect() |
|
493 if alignment == Qt.AlignLeft: |
|
494 if amount is None or rect.x() < amount: |
|
495 amount = rect.x() |
|
496 index = i |
|
497 elif alignment == Qt.AlignRight: |
|
498 if amount is None or rect.x() + rect.width() > amount: |
|
499 amount = rect.x() + rect.width() |
|
500 index = i |
|
501 elif alignment == Qt.AlignHCenter: |
|
502 if amount is None or rect.width() > amount: |
|
503 amount = rect.width() |
|
504 index = i |
|
505 elif alignment == Qt.AlignTop: |
|
506 if amount is None or rect.y() < amount: |
|
507 amount = rect.y() |
|
508 index = i |
|
509 elif alignment == Qt.AlignBottom: |
|
510 if amount is None or rect.y() + rect.height() > amount: |
|
511 amount = rect.y() + rect.height() |
|
512 index = i |
|
513 elif alignment == Qt.AlignVCenter: |
|
514 if amount is None or rect.height() > amount: |
|
515 amount = rect.height() |
|
516 index = i |
|
517 rect = items[index].sceneBoundingRect() |
|
518 |
|
519 # step 3: move the other items |
|
520 for i, item in enumerate(items): |
|
521 if i == index: |
|
522 continue |
|
523 itemrect = item.sceneBoundingRect() |
|
524 xOffset = yOffset = 0 |
|
525 if alignment == Qt.AlignLeft: |
|
526 xOffset = rect.x() - itemrect.x() |
|
527 elif alignment == Qt.AlignRight: |
|
528 xOffset = (rect.x() + rect.width()) - \ |
|
529 (itemrect.x() + itemrect.width()) |
|
530 elif alignment == Qt.AlignHCenter: |
|
531 xOffset = (rect.x() + rect.width() // 2) - \ |
|
532 (itemrect.x() + itemrect.width() // 2) |
|
533 elif alignment == Qt.AlignTop: |
|
534 yOffset = rect.y() - itemrect.y() |
|
535 elif alignment == Qt.AlignBottom: |
|
536 yOffset = (rect.y() + rect.height()) - \ |
|
537 (itemrect.y() + itemrect.height()) |
|
538 elif alignment == Qt.AlignVCenter: |
|
539 yOffset = (rect.y() + rect.height() // 2) - \ |
|
540 (itemrect.y() + itemrect.height() // 2) |
|
541 item.moveBy(xOffset, yOffset) |
|
542 |
|
543 self.scene().update() |
|
544 |
|
545 def __itemsBoundingRect(self, items): |
|
546 """ |
|
547 Private method to calculate the bounding rectangle of the given items. |
|
548 |
|
549 @param items list of items to operate on (list of UMLItem) |
|
550 @return bounding rectangle (QRectF) |
|
551 """ |
|
552 rect = self.scene().sceneRect() |
|
553 right = rect.left() |
|
554 bottom = rect.top() |
|
555 left = rect.right() |
|
556 top = rect.bottom() |
|
557 for item in items: |
|
558 rect = item.sceneBoundingRect() |
|
559 left = min(rect.left(), left) |
|
560 right = max(rect.right(), right) |
|
561 top = min(rect.top(), top) |
|
562 bottom = max(rect.bottom(), bottom) |
|
563 return QRectF(left, top, right - left, bottom - top) |
|
564 |
|
565 def keyPressEvent(self, evt): |
|
566 """ |
|
567 Protected method handling key press events. |
|
568 |
|
569 @param evt reference to the key event (QKeyEvent) |
|
570 """ |
|
571 key = evt.key() |
|
572 if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]: |
|
573 items = self.filteredItems(self.scene().selectedItems()) |
|
574 if items: |
|
575 if evt.modifiers() & Qt.ControlModifier: |
|
576 stepSize = 50 |
|
577 else: |
|
578 stepSize = 5 |
|
579 if key == Qt.Key_Up: |
|
580 dx = 0 |
|
581 dy = -stepSize |
|
582 elif key == Qt.Key_Down: |
|
583 dx = 0 |
|
584 dy = stepSize |
|
585 elif key == Qt.Key_Left: |
|
586 dx = -stepSize |
|
587 dy = 0 |
|
588 else: |
|
589 dx = stepSize |
|
590 dy = 0 |
|
591 for item in items: |
|
592 item.moveBy(dx, dy) |
|
593 evt.accept() |
|
594 return |
|
595 |
|
596 super(UMLGraphicsView, self).keyPressEvent(evt) |
|
597 |
|
598 def wheelEvent(self, evt): |
|
599 """ |
|
600 Protected method to handle wheel events. |
|
601 |
|
602 @param evt reference to the wheel event (QWheelEvent) |
|
603 """ |
|
604 if evt.modifiers() & Qt.ControlModifier: |
|
605 if qVersionTuple() >= (5, 0, 0): |
|
606 delta = evt.angleDelta().y() |
|
607 else: |
|
608 delta = evt.delta() |
|
609 if delta < 0: |
|
610 self.zoomOut() |
|
611 elif delta > 0: |
|
612 self.zoomIn() |
|
613 evt.accept() |
|
614 return |
|
615 |
|
616 super(UMLGraphicsView, self).wheelEvent(evt) |
|
617 |
|
618 def event(self, evt): |
|
619 """ |
|
620 Public method handling events. |
|
621 |
|
622 @param evt reference to the event (QEvent) |
|
623 @return flag indicating, if the event was handled (boolean) |
|
624 """ |
|
625 if evt.type() == QEvent.Gesture: |
|
626 self.gestureEvent(evt) |
|
627 return True |
|
628 |
|
629 return super(UMLGraphicsView, self).event(evt) |
|
630 |
|
631 def gestureEvent(self, evt): |
|
632 """ |
|
633 Protected method handling gesture events. |
|
634 |
|
635 @param evt reference to the gesture event (QGestureEvent |
|
636 """ |
|
637 pinch = evt.gesture(Qt.PinchGesture) |
|
638 if pinch: |
|
639 if pinch.state() == Qt.GestureStarted: |
|
640 pinch.setTotalScaleFactor(self.zoom() / 100.0) |
|
641 elif pinch.state() == Qt.GestureUpdated: |
|
642 self.setZoom(int(pinch.totalScaleFactor() * 100)) |
|
643 evt.accept() |
|
644 |
|
645 def getItemId(self): |
|
646 """ |
|
647 Public method to get the ID to be assigned to an item. |
|
648 |
|
649 @return item ID (integer) |
|
650 """ |
|
651 self.__itemId += 1 |
|
652 return self.__itemId |
|
653 |
|
654 def findItem(self, itemId): |
|
655 """ |
|
656 Public method to find an UML item based on the ID. |
|
657 |
|
658 @param itemId of the item to search for (integer) |
|
659 @return item found (UMLItem) or None |
|
660 """ |
|
661 for item in self.scene().items(): |
|
662 try: |
|
663 if item.getId() == itemId: |
|
664 return item |
|
665 except AttributeError: |
|
666 continue |
|
667 |
|
668 return None |
|
669 |
|
670 def findItemByName(self, name): |
|
671 """ |
|
672 Public method to find an UML item based on its name. |
|
673 |
|
674 @param name name to look for (string) |
|
675 @return item found (UMLItem) or None |
|
676 """ |
|
677 for item in self.scene().items(): |
|
678 try: |
|
679 if item.getName() == name: |
|
680 return item |
|
681 except AttributeError: |
|
682 continue |
|
683 |
|
684 return None |
|
685 |
|
686 def getPersistenceData(self): |
|
687 """ |
|
688 Public method to get a list of data to be persisted. |
|
689 |
|
690 @return list of data to be persisted (list of strings) |
|
691 """ |
|
692 lines = [ |
|
693 "diagram_name: {0}".format(self.diagramName), |
|
694 ] |
|
695 |
|
696 for item in self.filteredItems(self.scene().items(), UMLItem): |
|
697 lines.append("item: id={0}, x={1}, y={2}, item_type={3}{4}".format( |
|
698 item.getId(), item.x(), item.y(), item.getItemType(), |
|
699 item.buildItemDataString())) |
|
700 |
|
701 from .AssociationItem import AssociationItem |
|
702 for item in self.filteredItems(self.scene().items(), AssociationItem): |
|
703 lines.append("association: {0}".format( |
|
704 item.buildAssociationItemDataString())) |
|
705 |
|
706 return lines |
|
707 |
|
708 def parsePersistenceData(self, version, data): |
|
709 """ |
|
710 Public method to parse persisted data. |
|
711 |
|
712 @param version version of the data (string) |
|
713 @param data persisted data to be parsed (list of string) |
|
714 @return tuple of flag indicating success (boolean) and faulty line |
|
715 number (integer) |
|
716 """ |
|
717 umlItems = {} |
|
718 |
|
719 if not data[0].startswith("diagram_name:"): |
|
720 return False, 0 |
|
721 self.diagramName = data[0].split(": ", 1)[1].strip() |
|
722 |
|
723 from .ClassItem import ClassItem |
|
724 from .ModuleItem import ModuleItem |
|
725 from .PackageItem import PackageItem |
|
726 from .AssociationItem import AssociationItem |
|
727 |
|
728 linenum = 0 |
|
729 for line in data[1:]: |
|
730 linenum += 1 |
|
731 if not line.startswith(("item:", "association:")): |
|
732 return False, linenum |
|
733 |
|
734 key, value = line.split(": ", 1) |
|
735 if key == "item": |
|
736 itemId, x, y, itemType, itemData = value.split(", ", 4) |
|
737 try: |
|
738 itemId = int(itemId.split("=", 1)[1].strip()) |
|
739 x = float(x.split("=", 1)[1].strip()) |
|
740 y = float(y.split("=", 1)[1].strip()) |
|
741 itemType = itemType.split("=", 1)[1].strip() |
|
742 if itemType == ClassItem.ItemType: |
|
743 itm = ClassItem(x=x, y=y, scene=self.scene()) |
|
744 elif itemType == ModuleItem.ItemType: |
|
745 itm = ModuleItem(x=x, y=y, scene=self.scene()) |
|
746 elif itemType == PackageItem.ItemType: |
|
747 itm = PackageItem(x=x, y=y, scene=self.scene()) |
|
748 itm.setId(itemId) |
|
749 umlItems[itemId] = itm |
|
750 if not itm.parseItemDataString(version, itemData): |
|
751 return False, linenum |
|
752 except ValueError: |
|
753 return False, linenum |
|
754 elif key == "association": |
|
755 srcId, dstId, assocType, topToBottom = \ |
|
756 AssociationItem.parseAssociationItemDataString( |
|
757 value.strip()) |
|
758 assoc = AssociationItem(umlItems[srcId], umlItems[dstId], |
|
759 assocType, topToBottom) |
|
760 self.scene().addItem(assoc) |
|
761 |
|
762 return True, -1 |