eric7/Graphics/UMLDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8295
3f5e8b0a338e
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing UML like diagrams.
8 """
9
10 import enum
11 import json
12
13 from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo, QCoreApplication
14 from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene
15
16 from E5Gui import E5MessageBox, E5FileDialog
17 from E5Gui.E5MainWindow import E5MainWindow
18
19 import UI.Config
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
32
33
34 class UMLDialog(E5MainWindow):
35 """
36 Class implementing a dialog showing UML like diagrams.
37 """
38 FileVersions = ("1.0", )
39 JsonFileVersions = ("1.0", )
40
41 UMLDialogType2String = {
42 UMLDialogType.CLASS_DIAGRAM:
43 QCoreApplication.translate("UMLDialog", "Class Diagram"),
44 UMLDialogType.PACKAGE_DIAGRAM:
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 }
51
52 def __init__(self, diagramType, project, path="", parent=None,
53 initBuilder=True, **kwargs):
54 """
55 Constructor
56
57 @param diagramType type of the diagram
58 @type UMLDialogType
59 @param project reference to the project object
60 @type Project
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
65 @param initBuilder flag indicating to initialize the diagram
66 builder
67 @type bool
68 @keyparam kwargs diagram specific data
69 @type dict
70 """
71 super().__init__(parent)
72 self.setObjectName("UMLDialog")
73
74 self.__project = project
75 self.__diagramType = diagramType
76
77 from .UMLGraphicsView import UMLGraphicsView
78 self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
79 self.umlView = UMLGraphicsView(self.scene, parent=self)
80 self.builder = self.__diagramBuilder(
81 self.__diagramType, path, **kwargs)
82 if self.builder and initBuilder:
83 self.builder.initialize()
84
85 self.__fileName = ""
86
87 self.__initActions()
88 self.__initToolBars()
89
90 self.setCentralWidget(self.umlView)
91
92 self.umlView.relayout.connect(self.__relayout)
93
94 self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
95
96 def __getDiagramTitel(self, diagramType):
97 """
98 Private method to get a textual description for the diagram type.
99
100 @param diagramType diagram type string
101 @type str
102 @return titel of the diagram
103 @rtype str
104 """
105 return UMLDialog.UMLDialogType2String.get(
106 diagramType, self.tr("Illegal Diagram Type")
107 )
108
109 def __initActions(self):
110 """
111 Private slot to initialize the actions.
112 """
113 self.closeAct = QAction(
114 UI.PixmapCache.getIcon("close"),
115 self.tr("Close"), self)
116 self.closeAct.triggered.connect(self.close)
117
118 self.openAct = QAction(
119 UI.PixmapCache.getIcon("open"),
120 self.tr("Load"), self)
121 self.openAct.triggered.connect(self.load)
122
123 self.saveAct = QAction(
124 UI.PixmapCache.getIcon("fileSave"),
125 self.tr("Save"), self)
126 self.saveAct.triggered.connect(self.__save)
127
128 self.saveAsAct = QAction(
129 UI.PixmapCache.getIcon("fileSaveAs"),
130 self.tr("Save As..."), self)
131 self.saveAsAct.triggered.connect(self.__saveAs)
132
133 self.saveImageAct = QAction(
134 UI.PixmapCache.getIcon("fileSavePixmap"),
135 self.tr("Save as Image"), self)
136 self.saveImageAct.triggered.connect(self.umlView.saveImage)
137
138 self.printAct = QAction(
139 UI.PixmapCache.getIcon("print"),
140 self.tr("Print"), self)
141 self.printAct.triggered.connect(self.umlView.printDiagram)
142
143 self.printPreviewAct = QAction(
144 UI.PixmapCache.getIcon("printPreview"),
145 self.tr("Print Preview"), self)
146 self.printPreviewAct.triggered.connect(
147 self.umlView.printPreviewDiagram)
148
149 def __initToolBars(self):
150 """
151 Private slot to initialize the toolbars.
152 """
153 self.windowToolBar = QToolBar(self.tr("Window"), self)
154 self.windowToolBar.setIconSize(UI.Config.ToolBarIconSize)
155 self.windowToolBar.addAction(self.closeAct)
156
157 self.fileToolBar = QToolBar(self.tr("File"), self)
158 self.fileToolBar.setIconSize(UI.Config.ToolBarIconSize)
159 self.fileToolBar.addAction(self.openAct)
160 self.fileToolBar.addSeparator()
161 self.fileToolBar.addAction(self.saveAct)
162 self.fileToolBar.addAction(self.saveAsAct)
163 self.fileToolBar.addAction(self.saveImageAct)
164 self.fileToolBar.addSeparator()
165 self.fileToolBar.addAction(self.printPreviewAct)
166 self.fileToolBar.addAction(self.printAct)
167
168 self.umlToolBar = self.umlView.initToolBar()
169
170 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolBar)
171 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.windowToolBar)
172 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.umlToolBar)
173
174 def show(self, fromFile=False):
175 """
176 Public method to show the dialog.
177
178 @param fromFile flag indicating, that the diagram was loaded
179 from file
180 @type bool
181 """
182 if not fromFile and self.builder:
183 self.builder.buildDiagram()
184 super().show()
185
186 def __relayout(self):
187 """
188 Private method to re-layout the diagram.
189 """
190 if self.builder:
191 self.builder.buildDiagram()
192
193 def __diagramBuilder(self, diagramType, path, **kwargs):
194 """
195 Private method to instantiate a diagram builder object.
196
197 @param diagramType type of the diagram
198 @type UMLDialogType
199 @param path file or directory path to build the diagram from
200 @type str
201 @keyparam kwargs diagram specific data
202 @type dict
203 @return reference to the instantiated diagram builder
204 @rtype UMLDiagramBuilder
205 """
206 if diagramType == UMLDialogType.CLASS_DIAGRAM:
207 from .UMLClassDiagramBuilder import UMLClassDiagramBuilder
208 return UMLClassDiagramBuilder(
209 self, self.umlView, self.__project, path, **kwargs)
210 elif diagramType == UMLDialogType.PACKAGE_DIAGRAM:
211 from .PackageDiagramBuilder import PackageDiagramBuilder
212 return PackageDiagramBuilder(
213 self, self.umlView, self.__project, path, **kwargs)
214 elif diagramType == UMLDialogType.IMPORTS_DIAGRAM:
215 from .ImportsDiagramBuilder import ImportsDiagramBuilder
216 return ImportsDiagramBuilder(
217 self, self.umlView, self.__project, path, **kwargs)
218 elif diagramType == UMLDialogType.APPLICATION_DIAGRAM:
219 from .ApplicationDiagramBuilder import ApplicationDiagramBuilder
220 return ApplicationDiagramBuilder(
221 self, self.umlView, self.__project, **kwargs)
222 else:
223 return None
224
225 def __save(self):
226 """
227 Private slot to save the diagram with the current name.
228 """
229 self.__saveAs(self.__fileName)
230
231 @pyqtSlot()
232 def __saveAs(self, filename=""):
233 """
234 Private slot to save the diagram.
235
236 @param filename name of the file to write to
237 @type str
238 """
239 if not filename:
240 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
241 self,
242 self.tr("Save Diagram"),
243 "",
244 self.tr("Eric Graphics File (*.egj);;"
245 "Eric Text Graphics File (*.e5g);;"
246 "All Files (*)"),
247 "",
248 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
249 if not fname:
250 return
251 ext = QFileInfo(fname).suffix()
252 if not ext:
253 ex = selectedFilter.split("(*")[1].split(")")[0]
254 if ex:
255 fname += ex
256 if QFileInfo(fname).exists():
257 res = E5MessageBox.yesNo(
258 self,
259 self.tr("Save Diagram"),
260 self.tr("<p>The file <b>{0}</b> already exists."
261 " Overwrite it?</p>").format(fname),
262 icon=E5MessageBox.Warning)
263 if not res:
264 return
265 filename = fname
266
267 res = (
268 self.__writeLineBasedGraphicsFile(filename)
269 if filename.endswith(".e5g") else
270 # JSON format is the default
271 self.__writeJsonGraphicsFile(filename)
272 )
273
274 if res:
275 # save the file name only in case of success
276 self.__fileName = filename
277
278 # TODO: eric7: delete the current one
279 def load(self, filename=""):
280 """
281 Public method to load a diagram from a file.
282
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(
290 self,
291 self.tr("Load Diagram"),
292 "",
293 self.tr("Eric Graphics File (*.egj);;"
294 "Eric Text Graphics File (*.e5g);;"
295 "All Files (*)"))
296 if not filename:
297 # Canceled by user
298 return False
299
300 return (
301 self.__readLineBasedGraphicsFile(filename)
302 if filename.endswith(".e5g") else
303 # JSON format is the default
304 self.__readJsonGraphicsFile(filename)
305 )
306
307 #######################################################################
308 ## Methods to read and write eric graphics files of the old line
309 ## based file format.
310 #######################################################################
311
312 def __readLineBasedGraphicsFile(self, filename):
313 """
314 Private method to read an eric graphics file using the old line
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 """
322 try:
323 with open(filename, "r", encoding="utf-8") as f:
324 data = f.read()
325 except OSError as err:
326 E5MessageBox.critical(
327 self,
328 self.tr("Load Diagram"),
329 self.tr(
330 """<p>The file <b>{0}</b> could not be read.</p>"""
331 """<p>Reason: {1}</p>""").format(filename, str(err)))
332 return False
333
334 lines = data.splitlines()
335 if len(lines) < 3:
336 self.__showInvalidDataMessage(filename)
337 return False
338
339 try:
340 # step 1: check version
341 linenum = 0
342 key, value = lines[linenum].split(": ", 1)
343 if (
344 key.strip() != "version" or
345 value.strip() not in UMLDialog.FileVersions
346 ):
347 self.__showInvalidDataMessage(filename, linenum)
348 return False
349 else:
350 version = value
351
352 # step 2: extract diagram type
353 linenum += 1
354 key, value = lines[linenum].split(": ", 1)
355 if key.strip() != "diagram_type":
356 self.__showInvalidDataMessage(filename, linenum)
357 return False
358 try:
359 diagramType = value.strip().split(None, 1)[0]
360 self.__diagramType = UMLDialogType(int(diagramType))
361 except ValueError:
362 self.__showInvalidDataMessage(filename, linenum)
363 return False
364 self.scene.clear()
365 self.builder = self.__diagramBuilder(self.__diagramType, "")
366
367 # step 3: extract scene size
368 linenum += 1
369 key, value = lines[linenum].split(": ", 1)
370 if key.strip() != "scene_size":
371 self.__showInvalidDataMessage(filename, linenum)
372 return False
373 try:
374 width, height = [float(v.strip()) for v in value.split(";")]
375 except ValueError:
376 self.__showInvalidDataMessage(filename, linenum)
377 return False
378 self.umlView.setSceneSize(width, height)
379
380 # step 4: extract builder data if available
381 linenum += 1
382 key, value = lines[linenum].split(": ", 1)
383 if key.strip() == "builder_data":
384 ok = self.builder.parsePersistenceData(version, value)
385 if not ok:
386 self.__showInvalidDataMessage(filename, linenum)
387 return False
388 linenum += 1
389
390 # step 5: extract the graphics items
391 ok, vlinenum = self.umlView.parsePersistenceData(
392 version, lines[linenum:])
393 if not ok:
394 self.__showInvalidDataMessage(filename, linenum + vlinenum)
395 return False
396
397 except IndexError:
398 self.__showInvalidDataMessage(filename)
399 return False
400
401 # everything worked fine, so remember the file name and set the
402 # window title
403 self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
404 self.__fileName = filename
405
406 return True
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
444 def __showInvalidDataMessage(self, filename, linenum=-1):
445 """
446 Private slot to show a message dialog indicating an invalid data file.
447
448 @param filename name of the file containing the invalid data
449 @type str
450 @param linenum number of the invalid line
451 @type int
452 """
453 msg = (
454 self.tr("""<p>The file <b>{0}</b> does not contain"""
455 """ valid data.</p>""").format(filename)
456 if linenum < 0 else
457 self.tr("""<p>The file <b>{0}</b> does not contain"""
458 """ valid data.</p><p>Invalid line: {1}</p>"""
459 ).format(filename, linenum + 1)
460 )
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

eric ide

mercurial