|
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) |