eric6/Graphics/UMLDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing UML like diagrams.
8 """
9
10 from __future__ import unicode_literals
11
12 from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo
13 from PyQt5.QtWidgets import QAction, QToolBar, QGraphicsScene
14
15 from E5Gui import E5MessageBox, E5FileDialog
16 from E5Gui.E5MainWindow import E5MainWindow
17
18 import UI.Config
19 import UI.PixmapCache
20
21
22 class UMLDialog(E5MainWindow):
23 """
24 Class implementing a dialog showing UML like diagrams.
25 """
26 NoDiagram = 255
27 ClassDiagram = 0
28 PackageDiagram = 1
29 ImportsDiagram = 2
30 ApplicationDiagram = 3
31
32 FileVersions = ["1.0"]
33
34 def __init__(self, diagramType, project, path="", parent=None,
35 initBuilder=True, **kwargs):
36 """
37 Constructor
38
39 @param diagramType type of the diagram (one of ApplicationDiagram,
40 ClassDiagram, ImportsDiagram, NoDiagram, PackageDiagram)
41 @param project reference to the project object (Project)
42 @param path file or directory path to build the diagram from (string)
43 @param parent parent widget of the dialog (QWidget)
44 @keyparam initBuilder flag indicating to initialize the diagram
45 builder (boolean)
46 @keyparam kwargs diagram specific data
47 """
48 super(UMLDialog, self).__init__(parent)
49 self.setObjectName("UMLDialog")
50
51 self.__diagramType = diagramType
52 self.__project = project
53
54 from .UMLGraphicsView import UMLGraphicsView
55 self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
56 self.umlView = UMLGraphicsView(self.scene, parent=self)
57 self.builder = self.__diagramBuilder(
58 self.__diagramType, path, **kwargs)
59 if self.builder and initBuilder:
60 self.builder.initialize()
61
62 self.__fileName = ""
63
64 self.__initActions()
65 self.__initToolBars()
66
67 self.setCentralWidget(self.umlView)
68
69 self.umlView.relayout.connect(self.__relayout)
70
71 self.setWindowTitle(self.__diagramTypeString())
72
73 def __initActions(self):
74 """
75 Private slot to initialize the actions.
76 """
77 self.closeAct = \
78 QAction(UI.PixmapCache.getIcon("close.png"),
79 self.tr("Close"), self)
80 self.closeAct.triggered.connect(self.close)
81
82 self.openAct = \
83 QAction(UI.PixmapCache.getIcon("open.png"),
84 self.tr("Load"), self)
85 self.openAct.triggered.connect(self.load)
86
87 self.saveAct = \
88 QAction(UI.PixmapCache.getIcon("fileSave.png"),
89 self.tr("Save"), self)
90 self.saveAct.triggered.connect(self.__save)
91
92 self.saveAsAct = \
93 QAction(UI.PixmapCache.getIcon("fileSaveAs.png"),
94 self.tr("Save As..."), self)
95 self.saveAsAct.triggered.connect(self.__saveAs)
96
97 self.saveImageAct = \
98 QAction(UI.PixmapCache.getIcon("fileSavePixmap.png"),
99 self.tr("Save as Image"), self)
100 self.saveImageAct.triggered.connect(self.umlView.saveImage)
101
102 self.printAct = \
103 QAction(UI.PixmapCache.getIcon("print.png"),
104 self.tr("Print"), self)
105 self.printAct.triggered.connect(self.umlView.printDiagram)
106
107 self.printPreviewAct = \
108 QAction(UI.PixmapCache.getIcon("printPreview.png"),
109 self.tr("Print Preview"), self)
110 self.printPreviewAct.triggered.connect(
111 self.umlView.printPreviewDiagram)
112
113 def __initToolBars(self):
114 """
115 Private slot to initialize the toolbars.
116 """
117 self.windowToolBar = QToolBar(self.tr("Window"), self)
118 self.windowToolBar.setIconSize(UI.Config.ToolBarIconSize)
119 self.windowToolBar.addAction(self.closeAct)
120
121 self.fileToolBar = QToolBar(self.tr("File"), self)
122 self.fileToolBar.setIconSize(UI.Config.ToolBarIconSize)
123 self.fileToolBar.addAction(self.openAct)
124 self.fileToolBar.addSeparator()
125 self.fileToolBar.addAction(self.saveAct)
126 self.fileToolBar.addAction(self.saveAsAct)
127 self.fileToolBar.addAction(self.saveImageAct)
128 self.fileToolBar.addSeparator()
129 self.fileToolBar.addAction(self.printPreviewAct)
130 self.fileToolBar.addAction(self.printAct)
131
132 self.umlToolBar = self.umlView.initToolBar()
133
134 self.addToolBar(Qt.TopToolBarArea, self.fileToolBar)
135 self.addToolBar(Qt.TopToolBarArea, self.windowToolBar)
136 self.addToolBar(Qt.TopToolBarArea, self.umlToolBar)
137
138 def show(self, fromFile=False):
139 """
140 Public method to show the dialog.
141
142 @keyparam fromFile flag indicating, that the diagram was loaded
143 from file (boolean)
144 """
145 if not fromFile and self.builder:
146 self.builder.buildDiagram()
147 super(UMLDialog, self).show()
148
149 def __relayout(self):
150 """
151 Private method to relayout the diagram.
152 """
153 if self.builder:
154 self.builder.buildDiagram()
155
156 def __diagramBuilder(self, diagramType, path, **kwargs):
157 """
158 Private method to instantiate a diagram builder object.
159
160 @param diagramType type of the diagram
161 (one of ApplicationDiagram, ClassDiagram, ImportsDiagram,
162 PackageDiagram)
163 @param path file or directory path to build the diagram from (string)
164 @keyparam kwargs diagram specific data
165 @return reference to the instantiated diagram builder
166 @exception ValueError raised to indicate an illegal diagram type
167 """
168 if diagramType == UMLDialog.ClassDiagram:
169 from .UMLClassDiagramBuilder import UMLClassDiagramBuilder
170 return UMLClassDiagramBuilder(
171 self, self.umlView, self.__project, path, **kwargs)
172 elif diagramType == UMLDialog.PackageDiagram:
173 from .PackageDiagramBuilder import PackageDiagramBuilder
174 return PackageDiagramBuilder(
175 self, self.umlView, self.__project, path, **kwargs)
176 elif diagramType == UMLDialog.ImportsDiagram:
177 from .ImportsDiagramBuilder import ImportsDiagramBuilder
178 return ImportsDiagramBuilder(
179 self, self.umlView, self.__project, path, **kwargs)
180 elif diagramType == UMLDialog.ApplicationDiagram:
181 from .ApplicationDiagramBuilder import ApplicationDiagramBuilder
182 return ApplicationDiagramBuilder(
183 self, self.umlView, self.__project, **kwargs)
184 elif diagramType == UMLDialog.NoDiagram:
185 return None
186 else:
187 raise ValueError(self.tr(
188 "Illegal diagram type '{0}' given.").format(diagramType))
189
190 def __diagramTypeString(self):
191 """
192 Private method to generate a readable string for the diagram type.
193
194 @return readable type string (string)
195 """
196 if self.__diagramType == UMLDialog.ClassDiagram:
197 return "Class Diagram"
198 elif self.__diagramType == UMLDialog.PackageDiagram:
199 return "Package Diagram"
200 elif self.__diagramType == UMLDialog.ImportsDiagram:
201 return "Imports Diagram"
202 elif self.__diagramType == UMLDialog.ApplicationDiagram:
203 return "Application Diagram"
204 else:
205 return "Illegal Diagram Type"
206
207 def __save(self):
208 """
209 Private slot to save the diagram with the current name.
210 """
211 self.__saveAs(self.__fileName)
212
213 @pyqtSlot()
214 def __saveAs(self, filename=""):
215 """
216 Private slot to save the diagram.
217
218 @param filename name of the file to write to (string)
219 """
220 if not filename:
221 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
222 self,
223 self.tr("Save Diagram"),
224 "",
225 self.tr("Eric Graphics File (*.e5g);;All Files (*)"),
226 "",
227 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
228 if not fname:
229 return
230 ext = QFileInfo(fname).suffix()
231 if not ext:
232 ex = selectedFilter.split("(*")[1].split(")")[0]
233 if ex:
234 fname += ex
235 if QFileInfo(fname).exists():
236 res = E5MessageBox.yesNo(
237 self,
238 self.tr("Save Diagram"),
239 self.tr("<p>The file <b>{0}</b> already exists."
240 " Overwrite it?</p>").format(fname),
241 icon=E5MessageBox.Warning)
242 if not res:
243 return
244 filename = fname
245
246 lines = [
247 "version: 1.0",
248 "diagram_type: {0} ({1})".format(
249 self.__diagramType, self.__diagramTypeString()),
250 "scene_size: {0};{1}".format(self.scene.width(),
251 self.scene.height()),
252 ]
253 persistenceData = self.builder.getPersistenceData()
254 if persistenceData:
255 lines.append("builder_data: {0}".format(persistenceData))
256 lines.extend(self.umlView.getPersistenceData())
257
258 try:
259 f = open(filename, "w", encoding="utf-8")
260 f.write("\n".join(lines))
261 f.close()
262 except (IOError, OSError) as err:
263 E5MessageBox.critical(
264 self,
265 self.tr("Save Diagram"),
266 self.tr(
267 """<p>The file <b>{0}</b> could not be saved.</p>"""
268 """<p>Reason: {1}</p>""").format(filename, str(err)))
269 return
270
271 self.__fileName = filename
272
273 def load(self):
274 """
275 Public method to load a diagram from a file.
276
277 @return flag indicating success (boolean)
278 """
279 filename = E5FileDialog.getOpenFileName(
280 self,
281 self.tr("Load Diagram"),
282 "",
283 self.tr("Eric Graphics File (*.e5g);;All Files (*)"))
284 if not filename:
285 # Cancelled by user
286 return False
287
288 try:
289 f = open(filename, "r", encoding="utf-8")
290 data = f.read()
291 f.close()
292 except (IOError, OSError) as err:
293 E5MessageBox.critical(
294 self,
295 self.tr("Load Diagram"),
296 self.tr(
297 """<p>The file <b>{0}</b> could not be read.</p>"""
298 """<p>Reason: {1}</p>""").format(filename, str(err)))
299 return False
300
301 lines = data.splitlines()
302 if len(lines) < 3:
303 self.__showInvalidDataMessage(filename)
304 return False
305
306 try:
307 # step 1: check version
308 linenum = 0
309 key, value = lines[linenum].split(": ", 1)
310 if key.strip() != "version" or \
311 value.strip() not in UMLDialog.FileVersions:
312 self.__showInvalidDataMessage(filename, linenum)
313 return False
314 else:
315 version = value
316
317 # step 2: extract diagram type
318 linenum += 1
319 key, value = lines[linenum].split(": ", 1)
320 if key.strip() != "diagram_type":
321 self.__showInvalidDataMessage(filename, linenum)
322 return False
323 try:
324 self.__diagramType = int(value.strip().split(None, 1)[0])
325 except ValueError:
326 self.__showInvalidDataMessage(filename, linenum)
327 return False
328 self.scene.clear()
329 self.builder = self.__diagramBuilder(self.__diagramType, "")
330
331 # step 3: extract scene size
332 linenum += 1
333 key, value = lines[linenum].split(": ", 1)
334 if key.strip() != "scene_size":
335 self.__showInvalidDataMessage(filename, linenum)
336 return False
337 try:
338 width, height = [float(v.strip()) for v in value.split(";")]
339 except ValueError:
340 self.__showInvalidDataMessage(filename, linenum)
341 return False
342 self.umlView.setSceneSize(width, height)
343
344 # step 4: extract builder data if available
345 linenum += 1
346 key, value = lines[linenum].split(": ", 1)
347 if key.strip() == "builder_data":
348 ok = self.builder.parsePersistenceData(version, value)
349 if not ok:
350 self.__showInvalidDataMessage(filename, linenum)
351 return False
352 linenum += 1
353
354 # step 5: extract the graphics items
355 ok, vlinenum = self.umlView.parsePersistenceData(
356 version, lines[linenum:])
357 if not ok:
358 self.__showInvalidDataMessage(filename, linenum + vlinenum)
359 return False
360
361 except IndexError:
362 self.__showInvalidDataMessage(filename)
363 return False
364
365 # everything worked fine, so remember the file name and set the
366 # window title
367 self.setWindowTitle(self.__diagramTypeString())
368 self.__fileName = filename
369
370 return True
371
372 def __showInvalidDataMessage(self, filename, linenum=-1):
373 """
374 Private slot to show a message dialog indicating an invalid data file.
375
376 @param filename name of the file containing the invalid data (string)
377 @param linenum number of the invalid line (integer)
378 """
379 if linenum < 0:
380 msg = self.tr("""<p>The file <b>{0}</b> does not contain"""
381 """ valid data.</p>""").format(filename)
382 else:
383 msg = self.tr("""<p>The file <b>{0}</b> does not contain"""
384 """ valid data.</p><p>Invalid line: {1}</p>"""
385 ).format(filename, linenum + 1)
386 E5MessageBox.critical(self, self.tr("Load Diagram"), msg)

eric ide

mercurial