src/eric7/Graphics/UMLDialog.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9153
506e35e424d5
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2022 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 import pathlib
13
14 from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication
15 from PyQt6.QtGui import QAction
16 from PyQt6.QtWidgets import QToolBar, QGraphicsScene
17
18 from EricWidgets import EricMessageBox, EricFileDialog
19 from EricWidgets.EricMainWindow import EricMainWindow
20
21 import UI.Config
22 import UI.PixmapCache
23
24
25 class UMLDialogType(enum.Enum):
26 """
27 Class defining the UML dialog types.
28 """
29 CLASS_DIAGRAM = 0
30 PACKAGE_DIAGRAM = 1
31 IMPORTS_DIAGRAM = 2
32 APPLICATION_DIAGRAM = 3
33 NO_DIAGRAM = 255
34
35
36 class UMLDialog(EricMainWindow):
37 """
38 Class implementing a dialog showing UML like diagrams.
39 """
40 FileVersions = ("1.0", )
41 JsonFileVersions = ("1.0", )
42
43 UMLDialogType2String = {
44 UMLDialogType.CLASS_DIAGRAM:
45 QCoreApplication.translate("UMLDialog", "Class Diagram"),
46 UMLDialogType.PACKAGE_DIAGRAM:
47 QCoreApplication.translate("UMLDialog", "Package Diagram"),
48 UMLDialogType.IMPORTS_DIAGRAM:
49 QCoreApplication.translate("UMLDialog", "Imports Diagram"),
50 UMLDialogType.APPLICATION_DIAGRAM:
51 QCoreApplication.translate("UMLDialog", "Application Diagram"),
52 }
53
54 def __init__(self, diagramType, project, path="", parent=None,
55 initBuilder=True, **kwargs):
56 """
57 Constructor
58
59 @param diagramType type of the diagram
60 @type UMLDialogType
61 @param project reference to the project object
62 @type Project
63 @param path file or directory path to build the diagram from
64 @type str
65 @param parent parent widget of the dialog
66 @type QWidget
67 @param initBuilder flag indicating to initialize the diagram
68 builder
69 @type bool
70 @keyparam kwargs diagram specific data
71 @type dict
72 """
73 super().__init__(parent)
74 self.setObjectName("UMLDialog")
75
76 self.__project = project
77 self.__diagramType = diagramType
78
79 from .UMLGraphicsView import UMLGraphicsView
80 self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
81 self.umlView = UMLGraphicsView(self.scene, parent=self)
82 self.builder = self.__diagramBuilder(
83 self.__diagramType, path, **kwargs)
84 if self.builder and initBuilder:
85 self.builder.initialize()
86
87 self.__fileName = ""
88
89 self.__initActions()
90 self.__initToolBars()
91
92 self.setCentralWidget(self.umlView)
93
94 self.umlView.relayout.connect(self.__relayout)
95
96 self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
97
98 def __getDiagramTitel(self, diagramType):
99 """
100 Private method to get a textual description for the diagram type.
101
102 @param diagramType diagram type string
103 @type str
104 @return titel of the diagram
105 @rtype str
106 """
107 return UMLDialog.UMLDialogType2String.get(
108 diagramType, self.tr("Illegal Diagram Type")
109 )
110
111 def __initActions(self):
112 """
113 Private slot to initialize the actions.
114 """
115 self.closeAct = QAction(
116 UI.PixmapCache.getIcon("close"),
117 self.tr("Close"), self)
118 self.closeAct.triggered.connect(self.close)
119
120 self.openAct = QAction(
121 UI.PixmapCache.getIcon("open"),
122 self.tr("Load"), self)
123 self.openAct.triggered.connect(self.load)
124
125 self.saveAct = QAction(
126 UI.PixmapCache.getIcon("fileSave"),
127 self.tr("Save"), self)
128 self.saveAct.triggered.connect(self.__save)
129
130 self.saveAsAct = QAction(
131 UI.PixmapCache.getIcon("fileSaveAs"),
132 self.tr("Save As..."), self)
133 self.saveAsAct.triggered.connect(self.__saveAs)
134
135 self.saveImageAct = QAction(
136 UI.PixmapCache.getIcon("fileSavePixmap"),
137 self.tr("Save as Image"), self)
138 self.saveImageAct.triggered.connect(self.umlView.saveImage)
139
140 self.printAct = QAction(
141 UI.PixmapCache.getIcon("print"),
142 self.tr("Print"), self)
143 self.printAct.triggered.connect(self.umlView.printDiagram)
144
145 self.printPreviewAct = QAction(
146 UI.PixmapCache.getIcon("printPreview"),
147 self.tr("Print Preview"), self)
148 self.printPreviewAct.triggered.connect(
149 self.umlView.printPreviewDiagram)
150
151 def __initToolBars(self):
152 """
153 Private slot to initialize the toolbars.
154 """
155 self.windowToolBar = QToolBar(self.tr("Window"), self)
156 self.windowToolBar.setIconSize(UI.Config.ToolBarIconSize)
157 self.windowToolBar.addAction(self.closeAct)
158
159 self.fileToolBar = QToolBar(self.tr("File"), self)
160 self.fileToolBar.setIconSize(UI.Config.ToolBarIconSize)
161 self.fileToolBar.addAction(self.openAct)
162 self.fileToolBar.addSeparator()
163 self.fileToolBar.addAction(self.saveAct)
164 self.fileToolBar.addAction(self.saveAsAct)
165 self.fileToolBar.addAction(self.saveImageAct)
166 self.fileToolBar.addSeparator()
167 self.fileToolBar.addAction(self.printPreviewAct)
168 self.fileToolBar.addAction(self.printAct)
169
170 self.umlToolBar = self.umlView.initToolBar()
171
172 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolBar)
173 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.windowToolBar)
174 self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.umlToolBar)
175
176 def show(self, fromFile=False):
177 """
178 Public method to show the dialog.
179
180 @param fromFile flag indicating, that the diagram was loaded
181 from file
182 @type bool
183 """
184 if not fromFile and self.builder:
185 self.builder.buildDiagram()
186 super().show()
187
188 def __relayout(self):
189 """
190 Private method to re-layout the diagram.
191 """
192 if self.builder:
193 self.builder.buildDiagram()
194
195 def __diagramBuilder(self, diagramType, path, **kwargs):
196 """
197 Private method to instantiate a diagram builder object.
198
199 @param diagramType type of the diagram
200 @type UMLDialogType
201 @param path file or directory path to build the diagram from
202 @type str
203 @keyparam kwargs diagram specific data
204 @type dict
205 @return reference to the instantiated diagram builder
206 @rtype UMLDiagramBuilder
207 """
208 if diagramType == UMLDialogType.CLASS_DIAGRAM:
209 from .UMLClassDiagramBuilder import UMLClassDiagramBuilder
210 return UMLClassDiagramBuilder(
211 self, self.umlView, self.__project, path, **kwargs)
212 elif diagramType == UMLDialogType.PACKAGE_DIAGRAM:
213 from .PackageDiagramBuilder import PackageDiagramBuilder
214 return PackageDiagramBuilder(
215 self, self.umlView, self.__project, path, **kwargs)
216 elif diagramType == UMLDialogType.IMPORTS_DIAGRAM:
217 from .ImportsDiagramBuilder import ImportsDiagramBuilder
218 return ImportsDiagramBuilder(
219 self, self.umlView, self.__project, path, **kwargs)
220 elif diagramType == UMLDialogType.APPLICATION_DIAGRAM:
221 from .ApplicationDiagramBuilder import ApplicationDiagramBuilder
222 return ApplicationDiagramBuilder(
223 self, self.umlView, self.__project, **kwargs)
224 else:
225 return None
226
227 def __save(self):
228 """
229 Private slot to save the diagram with the current name.
230 """
231 self.__saveAs(self.__fileName)
232
233 @pyqtSlot()
234 def __saveAs(self, filename=""):
235 """
236 Private slot to save the diagram.
237
238 @param filename name of the file to write to
239 @type str
240 """
241 if not filename:
242 fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
243 self,
244 self.tr("Save Diagram"),
245 "",
246 self.tr("Eric Graphics File (*.egj);;"
247 "Eric Text Graphics File (*.e5g);;"
248 "All Files (*)"),
249 "",
250 EricFileDialog.DontConfirmOverwrite)
251 if not fname:
252 return
253
254 fpath = pathlib.Path(fname)
255 if not fpath.suffix:
256 ex = selectedFilter.split("(*")[1].split(")")[0]
257 if ex:
258 fpath = fpath.with_suffix(ex)
259 if fpath.exists():
260 res = EricMessageBox.yesNo(
261 self,
262 self.tr("Save Diagram"),
263 self.tr("<p>The file <b>{0}</b> already exists."
264 " Overwrite it?</p>").format(fpath),
265 icon=EricMessageBox.Warning)
266 if not res:
267 return
268 filename = str(fpath)
269
270 res = self.__writeJsonGraphicsFile(filename)
271
272 if res:
273 # save the file name only in case of success
274 self.__fileName = filename
275
276 # Note: remove loading of eric6 line based diagram format after 22.6
277 def load(self, filename=""):
278 """
279 Public method to load a diagram from a file.
280
281 @param filename name of the file to be loaded
282 @type str
283 @return flag indicating success
284 @rtype bool
285 """
286 if not filename:
287 filename = EricFileDialog.getOpenFileName(
288 self,
289 self.tr("Load Diagram"),
290 "",
291 self.tr("Eric Graphics File (*.egj);;"
292 "Eric Text Graphics File (*.e5g);;"
293 "All Files (*)"))
294 if not filename:
295 # Canceled by user
296 return False
297
298 return (
299 self.__readLineBasedGraphicsFile(filename)
300 if filename.endswith(".e5g") else
301 # JSON format is the default
302 self.__readJsonGraphicsFile(filename)
303 )
304
305 #######################################################################
306 ## Methods to read and write eric graphics files of the old line
307 ## based file format.
308 #######################################################################
309
310 def __readLineBasedGraphicsFile(self, filename):
311 """
312 Private method to read an eric graphics file using the old line
313 based file format.
314
315 @param filename name of the file to be read
316 @type str
317 @return flag indicating success
318 @rtype bool
319 """
320 try:
321 with open(filename, "r", encoding="utf-8") as f:
322 data = f.read()
323 except OSError as err:
324 EricMessageBox.critical(
325 self,
326 self.tr("Load Diagram"),
327 self.tr(
328 """<p>The file <b>{0}</b> could not be read.</p>"""
329 """<p>Reason: {1}</p>""").format(filename, str(err)))
330 return False
331
332 lines = data.splitlines()
333 if len(lines) < 3:
334 self.__showInvalidDataMessage(filename)
335 return False
336
337 try:
338 # step 1: check version
339 linenum = 0
340 key, value = lines[linenum].split(": ", 1)
341 if (
342 key.strip() != "version" or
343 value.strip() not in UMLDialog.FileVersions
344 ):
345 self.__showInvalidDataMessage(filename, linenum)
346 return False
347 else:
348 version = value
349
350 # step 2: extract diagram type
351 linenum += 1
352 key, value = lines[linenum].split(": ", 1)
353 if key.strip() != "diagram_type":
354 self.__showInvalidDataMessage(filename, linenum)
355 return False
356 try:
357 diagramType = value.strip().split(None, 1)[0]
358 self.__diagramType = UMLDialogType(int(diagramType))
359 except ValueError:
360 self.__showInvalidDataMessage(filename, linenum)
361 return False
362 self.scene.clear()
363 self.builder = self.__diagramBuilder(self.__diagramType, "")
364
365 # step 3: extract scene size
366 linenum += 1
367 key, value = lines[linenum].split(": ", 1)
368 if key.strip() != "scene_size":
369 self.__showInvalidDataMessage(filename, linenum)
370 return False
371 try:
372 width, height = [float(v.strip()) for v in value.split(";")]
373 except ValueError:
374 self.__showInvalidDataMessage(filename, linenum)
375 return False
376 self.umlView.setSceneSize(width, height)
377
378 # step 4: extract builder data if available
379 linenum += 1
380 key, value = lines[linenum].split(": ", 1)
381 if key.strip() == "builder_data":
382 ok = self.builder.parsePersistenceData(version, value)
383 if not ok:
384 self.__showInvalidDataMessage(filename, linenum)
385 return False
386 linenum += 1
387
388 # step 5: extract the graphics items
389 ok, vlinenum = self.umlView.parsePersistenceData(
390 version, lines[linenum:])
391 if not ok:
392 self.__showInvalidDataMessage(filename, linenum + vlinenum)
393 return False
394
395 except IndexError:
396 self.__showInvalidDataMessage(filename)
397 return False
398
399 # everything worked fine, so remember the file name and set the
400 # window title
401 self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
402 self.__fileName = filename
403
404 return True
405
406 def __showInvalidDataMessage(self, filename, linenum=-1):
407 """
408 Private slot to show a message dialog indicating an invalid data file.
409
410 @param filename name of the file containing the invalid data
411 @type str
412 @param linenum number of the invalid line
413 @type int
414 """
415 msg = (
416 self.tr("""<p>The file <b>{0}</b> does not contain"""
417 """ valid data.</p>""").format(filename)
418 if linenum < 0 else
419 self.tr("""<p>The file <b>{0}</b> does not contain"""
420 """ valid data.</p><p>Invalid line: {1}</p>"""
421 ).format(filename, linenum + 1)
422 )
423 EricMessageBox.critical(self, self.tr("Load Diagram"), msg)
424
425 #######################################################################
426 ## Methods to read and write eric graphics files of the JSON based
427 ## file format.
428 #######################################################################
429
430 def __writeJsonGraphicsFile(self, filename):
431 """
432 Private method to write an eric graphics file using the JSON based
433 file format.
434
435 @param filename name of the file to write to
436 @type str
437 @return flag indicating a successful write
438 @rtype bool
439 """
440 data = {
441 "version": "1.0",
442 "type": self.__diagramType.value,
443 "title": self.__getDiagramTitel(self.__diagramType),
444 "width": self.scene.width(),
445 "height": self.scene.height(),
446 "builder": self.builder.toDict(),
447 "view": self.umlView.toDict(),
448 }
449
450 try:
451 jsonString = json.dumps(data, indent=2)
452 with open(filename, "w") as f:
453 f.write(jsonString)
454 return True
455 except (TypeError, OSError) as err:
456 EricMessageBox.critical(
457 self,
458 self.tr("Save Diagram"),
459 self.tr(
460 """<p>The file <b>{0}</b> could not be saved.</p>"""
461 """<p>Reason: {1}</p>""").format(filename, str(err))
462 )
463 return False
464
465 def __readJsonGraphicsFile(self, filename):
466 """
467 Private method to read an eric graphics file using the JSON based
468 file format.
469
470 @param filename name of the file to be read
471 @type str
472 @return flag indicating a successful read
473 @rtype bool
474 """
475 try:
476 with open(filename, "r") as f:
477 jsonString = f.read()
478 data = json.loads(jsonString)
479 except (OSError, json.JSONDecodeError) as err:
480 EricMessageBox.critical(
481 None,
482 self.tr("Load Diagram"),
483 self.tr(
484 """<p>The file <b>{0}</b> could not be read.</p>"""
485 """<p>Reason: {1}</p>""").format(filename, str(err))
486 )
487 return False
488
489 try:
490 # step 1: check version
491 if data["version"] in UMLDialog.JsonFileVersions:
492 version = data["version"]
493 else:
494 self.__showInvalidDataMessage(filename)
495 return False
496
497 # step 2: set diagram type
498 try:
499 self.__diagramType = UMLDialogType(data["type"])
500 except ValueError:
501 self.__showInvalidDataMessage(filename)
502 return False
503 self.scene.clear()
504 self.builder = self.__diagramBuilder(self.__diagramType, "")
505
506 # step 3: set scene size
507 self.umlView.setSceneSize(data["width"], data["height"])
508
509 # step 4: extract builder data if available
510 ok, msg = self.builder.fromDict(version, data["builder"])
511 if not ok:
512 if msg:
513 res = EricMessageBox.warning(
514 self,
515 self.tr("Load Diagram"),
516 msg,
517 EricMessageBox.Abort | EricMessageBox.Ignore,
518 EricMessageBox.Abort)
519 if res == EricMessageBox.Abort:
520 return False
521 else:
522 self.umlView.setLayoutActionsEnabled(False)
523 else:
524 self.__showInvalidDataMessage(filename)
525 return False
526
527 # step 5: extract the graphics items
528 ok = self.umlView.fromDict(version, data["view"])
529 if not ok:
530 self.__showInvalidDataMessage(filename)
531 return False
532 except KeyError:
533 self.__showInvalidDataMessage(filename)
534 return False
535
536 # everything worked fine, so remember the file name and set the
537 # window title
538 self.setWindowTitle(self.__getDiagramTitel(self.__diagramType))
539 self.__fileName = filename
540
541 return True

eric ide

mercurial