eric6/Graphics/UMLDialog.py

branch
maintenance
changeset 8400
b3eefd7e58d1
parent 8273
698ae46f40a4
parent 8295
3f5e8b0a338e
equal deleted inserted replaced
8274:197414ba11cc 8400:b3eefd7e58d1
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(
71 89
72 self.setCentralWidget(self.umlView) 90 self.setCentralWidget(self.umlView)
73 91
74 self.umlView.relayout.connect(self.__relayout) 92 self.umlView.relayout.connect(self.__relayout)
75 93
76 self.setWindowTitle(self.__diagramTypeString) 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 )
77 108
78 def __initActions(self): 109 def __initActions(self):
79 """ 110 """
80 Private slot to initialize the actions. 111 Private slot to initialize the actions.
81 """ 112 """
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
201 """ 226 """
202 Private slot to save the diagram with the current name. 227 Private slot to save the diagram with the current name.
203 """ 228 """
204 self.__saveAs(self.__fileName) 229 self.__saveAs(self.__fileName)
205 230
206 # TODO: change this to save file in JSON format
207 @pyqtSlot() 231 @pyqtSlot()
208 def __saveAs(self, filename=""): 232 def __saveAs(self, filename=""):
209 """ 233 """
210 Private slot to save the diagram. 234 Private slot to save the diagram.
211 235
212 @param filename name of the file to write to (string) 236 @param filename name of the file to write to
237 @type str
213 """ 238 """
214 if not filename: 239 if not filename:
215 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( 240 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
216 self, 241 self,
217 self.tr("Save Diagram"), 242 self.tr("Save Diagram"),
218 "", 243 "",
219 self.tr("Eric Graphics File (*.e5g);;All Files (*)"), 244 self.tr("Eric Graphics File (*.egj);;"
245 "Eric Text Graphics File (*.e5g);;"
246 "All Files (*)"),
220 "", 247 "",
221 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) 248 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
222 if not fname: 249 if not fname:
223 return 250 return
224 ext = QFileInfo(fname).suffix() 251 ext = QFileInfo(fname).suffix()
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(
315 key, value = lines[linenum].split(": ", 1) 354 key, value = lines[linenum].split(": ", 1)
316 if key.strip() != "diagram_type": 355 if key.strip() != "diagram_type":
317 self.__showInvalidDataMessage(filename, linenum) 356 self.__showInvalidDataMessage(filename, linenum)
318 return False 357 return False
319 try: 358 try:
320 diagramType, diagramTypeString = value.strip().split(None, 1) 359 diagramType = value.strip().split(None, 1)[0]
321 self.__diagramType = int(self.__diagramType) 360 self.__diagramType = UMLDialogType(int(diagramType))
322 self.__diagramTypeString = diagramTypeString[1:-1]
323 # remove opening an closing bracket
324 except ValueError: 361 except ValueError:
325 self.__showInvalidDataMessage(filename, linenum) 362 self.__showInvalidDataMessage(filename, linenum)
326 return False 363 return False
327 self.scene.clear() 364 self.scene.clear()
328 self.builder = self.__diagramBuilder(self.__diagramType, "") 365 self.builder = self.__diagramBuilder(self.__diagramType, "")
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

eric ide

mercurial