5 |
5 |
6 """ |
6 """ |
7 Module implementing a dialog showing UML like diagrams. |
7 Module implementing a dialog showing UML like diagrams. |
8 """ |
8 """ |
9 |
9 |
10 from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo |
10 import enum |
|
11 import json |
|
12 |
|
13 from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo, QCoreApplication |
11 from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene |
14 from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene |
12 |
15 |
13 from E5Gui import E5MessageBox, E5FileDialog |
16 from E5Gui import E5MessageBox, E5FileDialog |
14 from E5Gui.E5MainWindow import E5MainWindow |
17 from E5Gui.E5MainWindow import E5MainWindow |
15 |
18 |
16 import UI.Config |
19 import UI.Config |
17 import UI.PixmapCache |
20 import UI.PixmapCache |
|
21 |
|
22 |
|
23 class UMLDialogType(enum.Enum): |
|
24 """ |
|
25 Class defining the UML dialog types. |
|
26 """ |
|
27 CLASS_DIAGRAM = 0 |
|
28 PACKAGE_DIAGRAM = 1 |
|
29 IMPORTS_DIAGRAM = 2 |
|
30 APPLICATION_DIAGRAM = 3 |
|
31 NO_DIAGRAM = 255 |
18 |
32 |
19 |
33 |
20 class UMLDialog(E5MainWindow): |
34 class UMLDialog(E5MainWindow): |
21 """ |
35 """ |
22 Class implementing a dialog showing UML like diagrams. |
36 Class implementing a dialog showing UML like diagrams. |
23 """ |
37 """ |
24 # convert to Enum |
38 FileVersions = ("1.0", ) |
25 NoDiagram = 255 |
39 JsonFileVersions = ("1.0", ) |
26 ClassDiagram = 0 |
40 |
27 PackageDiagram = 1 |
41 UMLDialogType2String = { |
28 ImportsDiagram = 2 |
42 UMLDialogType.CLASS_DIAGRAM: |
29 ApplicationDiagram = 3 |
43 QCoreApplication.translate("UMLDialog", "Class Diagram"), |
30 |
44 UMLDialogType.PACKAGE_DIAGRAM: |
31 FileVersions = ["1.0"] |
45 QCoreApplication.translate("UMLDialog", "Package Diagram"), |
|
46 UMLDialogType.IMPORTS_DIAGRAM: |
|
47 QCoreApplication.translate("UMLDialog", "Imports Diagram"), |
|
48 UMLDialogType.APPLICATION_DIAGRAM: |
|
49 QCoreApplication.translate("UMLDialog", "Application Diagram"), |
|
50 } |
32 |
51 |
33 def __init__(self, diagramType, project, path="", parent=None, |
52 def __init__(self, diagramType, project, path="", parent=None, |
34 initBuilder=True, **kwargs): |
53 initBuilder=True, **kwargs): |
35 """ |
54 """ |
36 Constructor |
55 Constructor |
37 |
56 |
38 @param diagramType type of the diagram (one of ApplicationDiagram, |
57 @param diagramType type of the diagram |
39 ClassDiagram, ImportsDiagram, NoDiagram, PackageDiagram) |
58 @type UMLDialogType |
40 @param project reference to the project object (Project) |
59 @param project reference to the project object |
41 @param path file or directory path to build the diagram from (string) |
60 @type Project |
42 @param parent parent widget of the dialog (QWidget) |
61 @param path file or directory path to build the diagram from |
|
62 @type str |
|
63 @param parent parent widget of the dialog |
|
64 @type QWidget |
43 @param initBuilder flag indicating to initialize the diagram |
65 @param initBuilder flag indicating to initialize the diagram |
44 builder (boolean) |
66 builder |
|
67 @type bool |
45 @keyparam kwargs diagram specific data |
68 @keyparam kwargs diagram specific data |
|
69 @type dict |
46 """ |
70 """ |
47 super().__init__(parent) |
71 super().__init__(parent) |
48 self.setObjectName("UMLDialog") |
72 self.setObjectName("UMLDialog") |
49 |
73 |
50 self.__project = project |
74 self.__project = project |
51 self.__diagramType = diagramType |
75 self.__diagramType = diagramType |
52 self.__diagramTypeString = { |
|
53 UMLDialog.ClassDiagram: "Class Diagram", |
|
54 UMLDialog.PackageDiagram: "Package Diagram", |
|
55 UMLDialog.ImportsDiagram: "Imports Diagram", |
|
56 UMLDialog.ApplicationDiagram: "Application Diagram", |
|
57 }.get(diagramType, "Illegal Diagram Type") |
|
58 |
76 |
59 from .UMLGraphicsView import UMLGraphicsView |
77 from .UMLGraphicsView import UMLGraphicsView |
60 self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0) |
78 self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0) |
61 self.umlView = UMLGraphicsView(self.scene, parent=self) |
79 self.umlView = UMLGraphicsView(self.scene, parent=self) |
62 self.builder = self.__diagramBuilder( |
80 self.builder = self.__diagramBuilder( |
143 def show(self, fromFile=False): |
174 def show(self, fromFile=False): |
144 """ |
175 """ |
145 Public method to show the dialog. |
176 Public method to show the dialog. |
146 |
177 |
147 @param fromFile flag indicating, that the diagram was loaded |
178 @param fromFile flag indicating, that the diagram was loaded |
148 from file (boolean) |
179 from file |
|
180 @type bool |
149 """ |
181 """ |
150 if not fromFile and self.builder: |
182 if not fromFile and self.builder: |
151 self.builder.buildDiagram() |
183 self.builder.buildDiagram() |
152 super().show() |
184 super().show() |
153 |
185 |
154 def __relayout(self): |
186 def __relayout(self): |
155 """ |
187 """ |
156 Private method to relayout the diagram. |
188 Private method to re-layout the diagram. |
157 """ |
189 """ |
158 if self.builder: |
190 if self.builder: |
159 self.builder.buildDiagram() |
191 self.builder.buildDiagram() |
160 |
192 |
161 def __diagramBuilder(self, diagramType, path, **kwargs): |
193 def __diagramBuilder(self, diagramType, path, **kwargs): |
162 """ |
194 """ |
163 Private method to instantiate a diagram builder object. |
195 Private method to instantiate a diagram builder object. |
164 |
196 |
165 @param diagramType type of the diagram |
197 @param diagramType type of the diagram |
166 (one of ApplicationDiagram, ClassDiagram, ImportsDiagram, |
198 @type UMLDialogType |
167 PackageDiagram) |
199 @param path file or directory path to build the diagram from |
168 @param path file or directory path to build the diagram from (string) |
200 @type str |
169 @keyparam kwargs diagram specific data |
201 @keyparam kwargs diagram specific data |
|
202 @type dict |
170 @return reference to the instantiated diagram builder |
203 @return reference to the instantiated diagram builder |
171 @exception ValueError raised to indicate an illegal diagram type |
204 @rtype UMLDiagramBuilder |
172 """ |
205 """ |
173 if diagramType not in ( |
206 if diagramType == UMLDialogType.CLASS_DIAGRAM: |
174 UMLDialog.ClassDiagram, UMLDialog.PackageDiagram, |
|
175 UMLDialog.ImportsDiagram, UMLDialog.ApplicationDiagram, |
|
176 UMLDialog.NoDiagram |
|
177 ): |
|
178 raise ValueError(self.tr( |
|
179 "Illegal diagram type '{0}' given.").format(diagramType)) |
|
180 |
|
181 if diagramType == UMLDialog.ClassDiagram: |
|
182 from .UMLClassDiagramBuilder import UMLClassDiagramBuilder |
207 from .UMLClassDiagramBuilder import UMLClassDiagramBuilder |
183 return UMLClassDiagramBuilder( |
208 return UMLClassDiagramBuilder( |
184 self, self.umlView, self.__project, path, **kwargs) |
209 self, self.umlView, self.__project, path, **kwargs) |
185 elif diagramType == UMLDialog.PackageDiagram: |
210 elif diagramType == UMLDialogType.PACKAGE_DIAGRAM: |
186 from .PackageDiagramBuilder import PackageDiagramBuilder |
211 from .PackageDiagramBuilder import PackageDiagramBuilder |
187 return PackageDiagramBuilder( |
212 return PackageDiagramBuilder( |
188 self, self.umlView, self.__project, path, **kwargs) |
213 self, self.umlView, self.__project, path, **kwargs) |
189 elif diagramType == UMLDialog.ImportsDiagram: |
214 elif diagramType == UMLDialogType.IMPORTS_DIAGRAM: |
190 from .ImportsDiagramBuilder import ImportsDiagramBuilder |
215 from .ImportsDiagramBuilder import ImportsDiagramBuilder |
191 return ImportsDiagramBuilder( |
216 return ImportsDiagramBuilder( |
192 self, self.umlView, self.__project, path, **kwargs) |
217 self, self.umlView, self.__project, path, **kwargs) |
193 elif diagramType == UMLDialog.ApplicationDiagram: |
218 elif diagramType == UMLDialogType.APPLICATION_DIAGRAM: |
194 from .ApplicationDiagramBuilder import ApplicationDiagramBuilder |
219 from .ApplicationDiagramBuilder import ApplicationDiagramBuilder |
195 return ApplicationDiagramBuilder( |
220 return ApplicationDiagramBuilder( |
196 self, self.umlView, self.__project, **kwargs) |
221 self, self.umlView, self.__project, **kwargs) |
197 else: |
222 else: |
198 return None |
223 return None |
235 icon=E5MessageBox.Warning) |
262 icon=E5MessageBox.Warning) |
236 if not res: |
263 if not res: |
237 return |
264 return |
238 filename = fname |
265 filename = fname |
239 |
266 |
240 lines = [ |
267 res = ( |
241 "version: 1.0", |
268 self.__writeLineBasedGraphicsFile(filename) |
242 "diagram_type: {0} ({1})".format( |
269 if filename.endswith(".e5g") else |
243 self.__diagramType, self.__diagramTypeString), |
270 # JSON format is the default |
244 "scene_size: {0};{1}".format(self.scene.width(), |
271 self.__writeJsonGraphicsFile(filename) |
245 self.scene.height()), |
272 ) |
246 ] |
273 |
247 persistenceData = self.builder.getPersistenceData() |
274 if res: |
248 if persistenceData: |
275 # save the file name only in case of success |
249 lines.append("builder_data: {0}".format(persistenceData)) |
276 self.__fileName = filename |
250 lines.extend(self.umlView.getPersistenceData()) |
277 |
251 |
278 # TODO: eric7: delete the current one |
252 try: |
279 def load(self, filename=""): |
253 with open(filename, "w", encoding="utf-8") as f: |
280 """ |
254 f.write("\n".join(lines)) |
281 Public method to load a diagram from a file. |
255 except OSError as err: |
282 |
256 E5MessageBox.critical( |
283 @param filename name of the file to be loaded |
|
284 @type str |
|
285 @return flag indicating success |
|
286 @rtype bool |
|
287 """ |
|
288 if not filename: |
|
289 filename = E5FileDialog.getOpenFileName( |
257 self, |
290 self, |
258 self.tr("Save Diagram"), |
291 self.tr("Load Diagram"), |
259 self.tr( |
292 "", |
260 """<p>The file <b>{0}</b> could not be saved.</p>""" |
293 self.tr("Eric Graphics File (*.egj);;" |
261 """<p>Reason: {1}</p>""").format(filename, str(err))) |
294 "Eric Text Graphics File (*.e5g);;" |
262 return |
295 "All Files (*)")) |
263 |
296 if not filename: |
264 self.__fileName = filename |
297 # Canceled by user |
265 |
298 return False |
266 # TODO: add loading of file in JSON format |
299 |
267 # TODO: delete the current one with eric7 |
300 return ( |
268 def load(self): |
301 self.__readLineBasedGraphicsFile(filename) |
269 """ |
302 if filename.endswith(".e5g") else |
270 Public method to load a diagram from a file. |
303 # JSON format is the default |
271 |
304 self.__readJsonGraphicsFile(filename) |
272 @return flag indicating success (boolean) |
305 ) |
273 """ |
306 |
274 filename = E5FileDialog.getOpenFileName( |
307 ####################################################################### |
275 self, |
308 ## Methods to read and write eric graphics files of the old line |
276 self.tr("Load Diagram"), |
309 ## based file format. |
277 "", |
310 ####################################################################### |
278 self.tr("Eric Graphics File (*.e5g);;All Files (*)")) |
311 |
279 if not filename: |
312 def __readLineBasedGraphicsFile(self, filename): |
280 # Cancelled by user |
313 """ |
281 return False |
314 Private method to read an eric graphics file using the old line |
282 |
315 based file format. |
|
316 |
|
317 @param filename name of the file to be read |
|
318 @type str |
|
319 @return flag indicating success |
|
320 @rtype bool |
|
321 """ |
283 try: |
322 try: |
284 with open(filename, "r", encoding="utf-8") as f: |
323 with open(filename, "r", encoding="utf-8") as f: |
285 data = f.read() |
324 data = f.read() |
286 except OSError as err: |
325 except OSError as err: |
287 E5MessageBox.critical( |
326 E5MessageBox.critical( |
361 self.__showInvalidDataMessage(filename) |
398 self.__showInvalidDataMessage(filename) |
362 return False |
399 return False |
363 |
400 |
364 # everything worked fine, so remember the file name and set the |
401 # everything worked fine, so remember the file name and set the |
365 # window title |
402 # window title |
366 self.setWindowTitle(self.__diagramTypeString) |
403 self.setWindowTitle(self.__getDiagramTitel(self.__diagramType)) |
367 self.__fileName = filename |
404 self.__fileName = filename |
368 |
405 |
369 return True |
406 return True |
370 |
407 |
|
408 def __writeLineBasedGraphicsFile(self, filename): |
|
409 """ |
|
410 Private method to write an eric graphics file using the old line |
|
411 based file format. |
|
412 |
|
413 @param filename name of the file to write to |
|
414 @type str |
|
415 @return flag indicating a successful write |
|
416 @rtype bool |
|
417 """ |
|
418 lines = [ |
|
419 "version: 1.0", |
|
420 "diagram_type: {0} ({1})".format( |
|
421 self.__diagramType.value, |
|
422 self.__getDiagramTitel(self.__diagramType)), |
|
423 "scene_size: {0};{1}".format(self.scene.width(), |
|
424 self.scene.height()), |
|
425 ] |
|
426 persistenceData = self.builder.getPersistenceData() |
|
427 if persistenceData: |
|
428 lines.append("builder_data: {0}".format(persistenceData)) |
|
429 lines.extend(self.umlView.getPersistenceData()) |
|
430 |
|
431 try: |
|
432 with open(filename, "w", encoding="utf-8") as f: |
|
433 f.write("\n".join(lines)) |
|
434 return True |
|
435 except OSError as err: |
|
436 E5MessageBox.critical( |
|
437 self, |
|
438 self.tr("Save Diagram"), |
|
439 self.tr( |
|
440 """<p>The file <b>{0}</b> could not be saved.</p>""" |
|
441 """<p>Reason: {1}</p>""").format(filename, str(err))) |
|
442 return False |
|
443 |
371 def __showInvalidDataMessage(self, filename, linenum=-1): |
444 def __showInvalidDataMessage(self, filename, linenum=-1): |
372 """ |
445 """ |
373 Private slot to show a message dialog indicating an invalid data file. |
446 Private slot to show a message dialog indicating an invalid data file. |
374 |
447 |
375 @param filename name of the file containing the invalid data (string) |
448 @param filename name of the file containing the invalid data |
376 @param linenum number of the invalid line (integer) |
449 @type str |
|
450 @param linenum number of the invalid line |
|
451 @type int |
377 """ |
452 """ |
378 msg = ( |
453 msg = ( |
379 self.tr("""<p>The file <b>{0}</b> does not contain""" |
454 self.tr("""<p>The file <b>{0}</b> does not contain""" |
380 """ valid data.</p>""").format(filename) |
455 """ valid data.</p>""").format(filename) |
381 if linenum < 0 else |
456 if linenum < 0 else |
382 self.tr("""<p>The file <b>{0}</b> does not contain""" |
457 self.tr("""<p>The file <b>{0}</b> does not contain""" |
383 """ valid data.</p><p>Invalid line: {1}</p>""" |
458 """ valid data.</p><p>Invalid line: {1}</p>""" |
384 ).format(filename, linenum + 1) |
459 ).format(filename, linenum + 1) |
385 ) |
460 ) |
386 E5MessageBox.critical(self, self.tr("Load Diagram"), msg) |
461 E5MessageBox.critical(self, self.tr("Load Diagram"), msg) |
|
462 |
|
463 ####################################################################### |
|
464 ## Methods to read and write eric graphics files of the JSON based |
|
465 ## file format. |
|
466 ####################################################################### |
|
467 |
|
468 def __writeJsonGraphicsFile(self, filename): |
|
469 """ |
|
470 Private method to write an eric graphics file using the JSON based |
|
471 file format. |
|
472 |
|
473 @param filename name of the file to write to |
|
474 @type str |
|
475 @return flag indicating a successful write |
|
476 @rtype bool |
|
477 """ |
|
478 data = { |
|
479 "version": "1.0", |
|
480 "type": self.__diagramType.value, |
|
481 "title": self.__getDiagramTitel(self.__diagramType), |
|
482 "width": self.scene.width(), |
|
483 "height": self.scene.height(), |
|
484 "builder": self.builder.toDict(), |
|
485 "view": self.umlView.toDict(), |
|
486 } |
|
487 |
|
488 try: |
|
489 jsonString = json.dumps(data, indent=2) |
|
490 with open(filename, "w") as f: |
|
491 f.write(jsonString) |
|
492 return True |
|
493 except (TypeError, OSError) as err: |
|
494 E5MessageBox.critical( |
|
495 self, |
|
496 self.tr("Save Diagram"), |
|
497 self.tr( |
|
498 """<p>The file <b>{0}</b> could not be saved.</p>""" |
|
499 """<p>Reason: {1}</p>""").format(filename, str(err)) |
|
500 ) |
|
501 return False |
|
502 |
|
503 def __readJsonGraphicsFile(self, filename): |
|
504 """ |
|
505 Private method to read an eric graphics file using the JSON based |
|
506 file format. |
|
507 |
|
508 @param filename name of the file to be read |
|
509 @type str |
|
510 @return flag indicating a successful read |
|
511 @rtype bool |
|
512 """ |
|
513 try: |
|
514 with open(filename, "r") as f: |
|
515 jsonString = f.read() |
|
516 data = json.loads(jsonString) |
|
517 except (OSError, json.JSONDecodeError) as err: |
|
518 E5MessageBox.critical( |
|
519 None, |
|
520 self.tr("Load Diagram"), |
|
521 self.tr( |
|
522 """<p>The file <b>{0}</b> could not be read.</p>""" |
|
523 """<p>Reason: {1}</p>""").format(filename, str(err)) |
|
524 ) |
|
525 return False |
|
526 |
|
527 try: |
|
528 # step 1: check version |
|
529 if data["version"] in UMLDialog.JsonFileVersions: |
|
530 version = data["version"] |
|
531 else: |
|
532 self.__showInvalidDataMessage(filename) |
|
533 return False |
|
534 |
|
535 # step 2: set diagram type |
|
536 try: |
|
537 self.__diagramType = UMLDialogType(data["type"]) |
|
538 except ValueError: |
|
539 self.__showInvalidDataMessage(filename) |
|
540 return False |
|
541 self.scene.clear() |
|
542 self.builder = self.__diagramBuilder(self.__diagramType, "") |
|
543 |
|
544 # step 3: set scene size |
|
545 self.umlView.setSceneSize(data["width"], data["height"]) |
|
546 |
|
547 # step 4: extract builder data if available |
|
548 ok, msg = self.builder.fromDict(version, data["builder"]) |
|
549 if not ok: |
|
550 if msg: |
|
551 res = E5MessageBox.warning( |
|
552 self, |
|
553 self.tr("Load Diagram"), |
|
554 msg, |
|
555 E5MessageBox.StandardButtons( |
|
556 E5MessageBox.Abort | |
|
557 E5MessageBox.Ignore), |
|
558 E5MessageBox.Abort) |
|
559 if res == E5MessageBox.Abort: |
|
560 return False |
|
561 else: |
|
562 self.umlView.setLayoutActionsEnabled(False) |
|
563 else: |
|
564 self.__showInvalidDataMessage(filename) |
|
565 return False |
|
566 |
|
567 # step 5: extract the graphics items |
|
568 ok = self.umlView.fromDict(version, data["view"]) |
|
569 if not ok: |
|
570 self.__showInvalidDataMessage(filename) |
|
571 return False |
|
572 except KeyError: |
|
573 self.__showInvalidDataMessage(filename) |
|
574 return False |
|
575 |
|
576 # everything worked fine, so remember the file name and set the |
|
577 # window title |
|
578 self.setWindowTitle(self.__getDiagramTitel(self.__diagramType)) |
|
579 self.__fileName = filename |
|
580 |
|
581 return True |