|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to generate code for a Qt4 dialog. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import sys |
|
12 |
|
13 from PyQt4.QtCore import * |
|
14 from PyQt4.QtGui import * |
|
15 from PyQt4 import uic |
|
16 |
|
17 from E4Gui.E4Application import e4App |
|
18 |
|
19 from NewDialogClassDialog import NewDialogClassDialog |
|
20 from Ui_CreateDialogCodeDialog import Ui_CreateDialogCodeDialog |
|
21 |
|
22 from Utilities import ModuleParser |
|
23 |
|
24 import UI.PixmapCache |
|
25 |
|
26 from eric4config import getConfig |
|
27 |
|
28 pyqtSignatureRole = Qt.UserRole + 1 |
|
29 pythonSignatureRole = Qt.UserRole + 2 |
|
30 rubySignatureRole = Qt.UserRole + 3 |
|
31 |
|
32 class CreateDialogCodeDialog(QDialog, Ui_CreateDialogCodeDialog): |
|
33 """ |
|
34 Class implementing a dialog to generate code for a Qt4 dialog. |
|
35 """ |
|
36 def __init__(self, formName, project, parent = None): |
|
37 """ |
|
38 Constructor |
|
39 |
|
40 @param formName name of the file containing the form (string) |
|
41 @param project reference to the project object |
|
42 @param parent parent widget if the dialog (QWidget) |
|
43 """ |
|
44 QDialog.__init__(self, parent) |
|
45 self.setupUi(self) |
|
46 |
|
47 self.okButton = self.buttonBox.button(QDialogButtonBox.Ok) |
|
48 |
|
49 self.slotsView.header().hide() |
|
50 |
|
51 self.project = project |
|
52 |
|
53 self.formFile = formName |
|
54 filename, ext = os.path.splitext(self.formFile) |
|
55 self.srcFile = '%s%s' % (filename, self.project.getDefaultSourceExtension()) |
|
56 |
|
57 self.slotsModel = QStandardItemModel() |
|
58 self.proxyModel = QSortFilterProxyModel() |
|
59 self.proxyModel.setDynamicSortFilter(True) |
|
60 self.proxyModel.setSourceModel(self.slotsModel) |
|
61 self.slotsView.setModel(self.proxyModel) |
|
62 |
|
63 self.clearFilterButton.setIcon(UI.PixmapCache.getIcon("clearLeft.png")) |
|
64 |
|
65 # initialize some member variables |
|
66 self.__initError = False |
|
67 self.__module = None |
|
68 |
|
69 if os.path.exists(self.srcFile): |
|
70 vm = e4App().getObject("ViewManager") |
|
71 ed = vm.getOpenEditor(self.srcFile) |
|
72 if ed and not vm.checkDirty(ed): |
|
73 self.__initError = True |
|
74 return |
|
75 |
|
76 self.__module = ModuleParser.readModule(self.srcFile, caching = False) |
|
77 |
|
78 if self.__module is not None: |
|
79 self.filenameEdit.setText(self.srcFile) |
|
80 |
|
81 classesList = [] |
|
82 for cls in self.__module.classes.values(): |
|
83 classesList.append(cls.name) |
|
84 classesList.sort() |
|
85 self.classNameCombo.addItems(classesList) |
|
86 |
|
87 if os.path.exists(self.srcFile) and self.classNameCombo.count() == 0: |
|
88 self.__initError = True |
|
89 QMessageBox.critical(None, |
|
90 self.trUtf8("Create Dialog Code"), |
|
91 self.trUtf8("""The file <b>{0}</b> exists but does not contain""" |
|
92 """ any classes.""").format(self.srcFile), |
|
93 QMessageBox.StandardButtons(\ |
|
94 QMessageBox.Abort)) |
|
95 |
|
96 self.okButton.setEnabled(self.classNameCombo.count() > 0) |
|
97 |
|
98 self.__updateSlotsModel() |
|
99 |
|
100 def initError(self): |
|
101 """ |
|
102 Public method to determine, if there was an initialzation error. |
|
103 |
|
104 @return flag indicating an initialzation error (boolean) |
|
105 """ |
|
106 return self.__initError |
|
107 |
|
108 def __objectName(self): |
|
109 """ |
|
110 Private method to get the object name of the dialog. |
|
111 |
|
112 @return object name (string) |
|
113 """ |
|
114 try: |
|
115 dlg = uic.loadUi(self.formFile) |
|
116 return dlg.objectName() |
|
117 except AttributeError, err: |
|
118 QMessageBox.critical(self, |
|
119 self.trUtf8("uic error"), |
|
120 self.trUtf8("""<p>There was an error loading the form <b>{0}</b>.</p>""" |
|
121 """<p>{1}</p>""").format(self.formFile, unicode(err)), |
|
122 QMessageBox.StandardButtons(\ |
|
123 QMessageBox.Ok)) |
|
124 return "" |
|
125 |
|
126 def __className(self): |
|
127 """ |
|
128 Private method to get the class name of the dialog. |
|
129 |
|
130 @return class name (sting) |
|
131 """ |
|
132 try: |
|
133 dlg = uic.loadUi(self.formFile) |
|
134 return dlg.metaObject().className() |
|
135 except AttributeError, err: |
|
136 QMessageBox.critical(self, |
|
137 self.trUtf8("uic error"), |
|
138 self.trUtf8("""<p>There was an error loading the form <b>{0}</b>.</p>""" |
|
139 """<p>{1}</p>""").format(self.formFile, unicode(err)), |
|
140 QMessageBox.StandardButtons(\ |
|
141 QMessageBox.Ok)) |
|
142 return "" |
|
143 |
|
144 def __signatures(self): |
|
145 """ |
|
146 Private slot to get the signatures. |
|
147 |
|
148 @return list of signatures (list of strings) |
|
149 """ |
|
150 if self.__module is None: |
|
151 return [] |
|
152 |
|
153 signatures = [] |
|
154 clsName = self.classNameCombo.currentText() |
|
155 if clsName: |
|
156 cls = self.__module.classes[clsName] |
|
157 for meth in cls.methods.values(): |
|
158 if meth.name.startswith("on_"): |
|
159 if meth.pyqtSignature is not None: |
|
160 sig = ", ".join([str(QMetaObject.normalizedType(t)) \ |
|
161 for t in meth.pyqtSignature.split(",")]) |
|
162 signatures.append("%s(%s)" % (meth.name, sig)) |
|
163 else: |
|
164 signatures.append(meth.name) |
|
165 return signatures |
|
166 |
|
167 def __updateSlotsModel(self): |
|
168 """ |
|
169 Private slot to update the slots tree display. |
|
170 """ |
|
171 self.filterEdit.clear() |
|
172 |
|
173 try: |
|
174 dlg = uic.loadUi(self.formFile) |
|
175 objects = dlg.findChildren(QWidget) + dlg.findChildren(QAction) |
|
176 |
|
177 signatureList = self.__signatures() |
|
178 |
|
179 self.slotsModel.clear() |
|
180 self.slotsModel.setHorizontalHeaderLabels([""]) |
|
181 for obj in objects: |
|
182 name = obj.objectName() |
|
183 if not name: |
|
184 continue |
|
185 |
|
186 metaObject = obj.metaObject() |
|
187 className = metaObject.className() |
|
188 itm = QStandardItem("%s (%s)" % (name, className)) |
|
189 self.slotsModel.appendRow(itm) |
|
190 for index in range(metaObject.methodCount()): |
|
191 metaMethod = metaObject.method(index) |
|
192 if metaMethod.methodType() == QMetaMethod.Signal: |
|
193 itm2 = QStandardItem("on_%s_%s" % (name, metaMethod.signature())) |
|
194 itm.appendRow(itm2) |
|
195 if self.__module is not None: |
|
196 method = "on_%s_%s" % \ |
|
197 (name, metaMethod.signature().split("(")[0]) |
|
198 method2 = "on_%s_%s" % \ |
|
199 (name, |
|
200 QMetaObject.normalizedSignature(metaMethod.signature())) |
|
201 |
|
202 if method2 in signatureList or method in signatureList: |
|
203 itm2.setFlags(Qt.ItemFlags(Qt.ItemIsEnabled)) |
|
204 itm2.setCheckState(Qt.Checked) |
|
205 itm2.setForeground(QBrush(Qt.blue)) |
|
206 continue |
|
207 |
|
208 pyqtSignature = \ |
|
209 ", ".join([str(t) for t in metaMethod.parameterTypes()]) |
|
210 |
|
211 parameterNames = metaMethod.parameterNames() |
|
212 if parameterNames: |
|
213 for index in range(len(parameterNames)): |
|
214 if not parameterNames[index]: |
|
215 parameterNames[index] = QByteArray("p%d" % index) |
|
216 methNamesSig = \ |
|
217 ", ".join([str(n) for n in parameterNames]) |
|
218 |
|
219 if methNamesSig: |
|
220 pythonSignature = "on_%s_%s(self, %s)" % \ |
|
221 (name, |
|
222 metaMethod.signature().split("(")[0], |
|
223 methNamesSig |
|
224 ) |
|
225 else: |
|
226 pythonSignature = "on_%s_%s(self)" % \ |
|
227 (name, |
|
228 metaMethod.signature().split("(")[0] |
|
229 ) |
|
230 itm2.setData(QVariant(pyqtSignature), pyqtSignatureRole) |
|
231 itm2.setData(QVariant(pythonSignature), pythonSignatureRole) |
|
232 |
|
233 itm2.setFlags(Qt.ItemFlags(\ |
|
234 Qt.ItemIsUserCheckable | \ |
|
235 Qt.ItemIsEnabled | \ |
|
236 Qt.ItemIsSelectable) |
|
237 ) |
|
238 itm2.setCheckState(Qt.Unchecked) |
|
239 |
|
240 self.slotsView.sortByColumn(0, Qt.AscendingOrder) |
|
241 except (AttributeError, ImportError), err: |
|
242 QMessageBox.critical(self, |
|
243 self.trUtf8("uic error"), |
|
244 self.trUtf8("""<p>There was an error loading the form <b>{0}</b>.</p>""" |
|
245 """<p>{1}</p>""").format(self.formFile, unicode(err)), |
|
246 QMessageBox.StandardButtons(\ |
|
247 QMessageBox.Ok)) |
|
248 |
|
249 def __generateCode(self): |
|
250 """ |
|
251 Private slot to generate the code as requested by the user. |
|
252 """ |
|
253 # first decide on extension |
|
254 if self.filenameEdit.text().endswith(".py") or \ |
|
255 self.filenameEdit.text().endswith(".pyw"): |
|
256 self.__generatePythonCode() |
|
257 elif self.filenameEdit.text().endswith(".rb"): |
|
258 pass |
|
259 # second decide on project language |
|
260 elif self.project.getProjectLanguage() in ["Python", "Python3"]: |
|
261 self.__generatePythonCode() |
|
262 elif self.project.getProjectLanguage() == "Ruby": |
|
263 pass |
|
264 else: |
|
265 # assume Python (our global default) |
|
266 self.__generatePythonCode() |
|
267 |
|
268 def __generatePythonCode(self): |
|
269 """ |
|
270 Private slot to generate Python code as requested by the user. |
|
271 """ |
|
272 # init some variables |
|
273 sourceImpl = [] |
|
274 appendAtIndex = -1 |
|
275 indentStr = " " |
|
276 slotsCode = [] |
|
277 |
|
278 if self.__module is None: |
|
279 # new file |
|
280 try: |
|
281 tmplName = os.path.join(getConfig('ericCodeTemplatesDir'), "impl.py.tmpl") |
|
282 tmplFile = open(tmplName, 'rb') |
|
283 template = tmplFile.read() |
|
284 tmplFile.close() |
|
285 except IOError, why: |
|
286 QMessageBox.critical(self, |
|
287 self.trUtf8("Code Generation"), |
|
288 self.trUtf8("""<p>Could not open the code template file "{0}".</p>""" |
|
289 """<p>Reason: {1}</p>""")\ |
|
290 .format(tmplName, unicode(why)), |
|
291 QMessageBox.StandardButtons(\ |
|
292 QMessageBox.Ok)) |
|
293 return |
|
294 |
|
295 objName = self.__objectName() |
|
296 if objName: |
|
297 template = template\ |
|
298 .replace("$FORMFILE$", |
|
299 os.path.splitext(os.path.basename(self.formFile))[0])\ |
|
300 .replace("$FORMCLASS$", objName)\ |
|
301 .replace("$CLASSNAME$", self.classNameCombo.currentText())\ |
|
302 .replace("$SUPERCLASS$", self.__className()) |
|
303 |
|
304 sourceImpl = template.splitlines(True) |
|
305 appendAtIndex = -1 |
|
306 |
|
307 # determine indent string |
|
308 for line in sourceImpl: |
|
309 if line.lstrip().startswith("def __init__"): |
|
310 indentStr = line.replace(line.lstrip(), "") |
|
311 break |
|
312 else: |
|
313 # extend existing file |
|
314 try: |
|
315 srcFile = open(self.srcFile, 'rb') |
|
316 sourceImpl = srcFile.readlines() |
|
317 srcFile.close() |
|
318 if not sourceImpl[-1].endswith(os.linesep): |
|
319 sourceImpl[-1] = "%s%s" % (sourceImpl[-1], os.linesep) |
|
320 except IOError, why: |
|
321 QMessageBox.critical(self, |
|
322 self.trUtf8("Code Generation"), |
|
323 self.trUtf8("""<p>Could not open the source file "{0}".</p>""" |
|
324 """<p>Reason: {1}</p>""")\ |
|
325 .format(self.srcFile, unicode(why)), |
|
326 QMessageBox.StandardButtons(\ |
|
327 QMessageBox.Ok)) |
|
328 return |
|
329 |
|
330 cls = self.__module.classes[self.classNameCombo.currentText()] |
|
331 if cls.endlineno == len(sourceImpl) or cls.endlineno == -1: |
|
332 appendAtIndex = -1 |
|
333 # delete empty lines at end |
|
334 while not sourceImpl[-1].strip(): |
|
335 del sourceImpl[-1] |
|
336 else: |
|
337 appendAtIndex = cls.endlineno - 1 |
|
338 |
|
339 # determine indent string |
|
340 for line in sourceImpl[cls.lineno:cls.endlineno+1]: |
|
341 if line.lstrip().startswith("def __init__"): |
|
342 indentStr = line.replace(line.lstrip(), "") |
|
343 break |
|
344 |
|
345 # do the coding stuff |
|
346 for row in range(self.slotsModel.rowCount()): |
|
347 topItem = self.slotsModel.item(row) |
|
348 for childRow in range(topItem.rowCount()): |
|
349 child = topItem.child(childRow) |
|
350 if child.checkState() and \ |
|
351 child.flags() & Qt.ItemFlags(Qt.ItemIsUserCheckable): |
|
352 slotsCode.append('%s\n' % indentStr) |
|
353 # TODO: adjust to new signal/slot mechanism |
|
354 slotsCode.append('%s@pyqtSlot(%s)\n' % \ |
|
355 (indentStr, child.data(pyqtSignatureRole).toString())) |
|
356 slotsCode.append('%sdef %s:\n' % \ |
|
357 (indentStr, child.data(pythonSignatureRole).toString())) |
|
358 slotsCode.append('%s"""\n' % (indentStr * 2,)) |
|
359 slotsCode.append('%sSlot documentation goes here.\n' % \ |
|
360 (indentStr * 2,)) |
|
361 slotsCode.append('%s"""\n' % (indentStr * 2,)) |
|
362 slotsCode.append('%s# %s: not implemented yet\n' % \ |
|
363 (indentStr * 2, "TODO")) |
|
364 slotsCode.append('%sraise NotImplementedError\n' % (indentStr * 2,)) |
|
365 |
|
366 if appendAtIndex == -1: |
|
367 sourceImpl.extend(slotsCode) |
|
368 else: |
|
369 sourceImpl[appendAtIndex:appendAtIndex] = slotsCode |
|
370 |
|
371 # write the new code |
|
372 try: |
|
373 srcFile = open(self.filenameEdit.text(), 'wb') |
|
374 srcFile.write("".join(sourceImpl)) |
|
375 srcFile.close() |
|
376 except IOError, why: |
|
377 QMessageBox.critical(self, |
|
378 self.trUtf8("Code Generation"), |
|
379 self.trUtf8("""<p>Could not write the source file "{0}".</p>""" |
|
380 """<p>Reason: {1}</p>""")\ |
|
381 .format(self.filenameEdit.text(), unicode(why)), |
|
382 QMessageBox.StandardButtons(\ |
|
383 QMessageBox.Ok)) |
|
384 return |
|
385 |
|
386 self.project.appendFile(unicode(self.filenameEdit.text())) |
|
387 |
|
388 @pyqtSlot(int) |
|
389 def on_classNameCombo_activated(self, index): |
|
390 """ |
|
391 Private slot to handle the activated signal of the classname combo. |
|
392 |
|
393 @param index index of the activated item (integer) |
|
394 """ |
|
395 self.__updateSlotsModel() |
|
396 |
|
397 def on_filterEdit_textChanged(self, text): |
|
398 """ |
|
399 Private slot called, when thext of the filter edit has changed. |
|
400 |
|
401 @param text changed text (string) |
|
402 """ |
|
403 re = QRegExp(text, Qt.CaseInsensitive, QRegExp.RegExp2) |
|
404 self.proxyModel.setFilterRegExp(re) |
|
405 |
|
406 @pyqtSlot() |
|
407 def on_clearFilterButton_clicked(self): |
|
408 """ |
|
409 Private slot called by a click of the clear filter button. |
|
410 """ |
|
411 self.filterEdit.clear() |
|
412 |
|
413 @pyqtSlot() |
|
414 def on_newButton_clicked(self): |
|
415 """ |
|
416 Private slot called to enter the data for a new dialog class. |
|
417 """ |
|
418 path, file = os.path.split(self.srcFile) |
|
419 objName = self.__objectName() |
|
420 if objName: |
|
421 dlg = NewDialogClassDialog(objName, file, path, self) |
|
422 if dlg.exec_() == QDialog.Accepted: |
|
423 className, fileName = dlg.getData() |
|
424 |
|
425 self.classNameCombo.clear() |
|
426 self.classNameCombo.addItem(className) |
|
427 self.srcFile = fileName |
|
428 self.filenameEdit.setText(self.srcFile) |
|
429 self.__module = None |
|
430 |
|
431 self.okButton.setEnabled(self.classNameCombo.count() > 0) |
|
432 |
|
433 def on_buttonBox_clicked(self, button): |
|
434 """ |
|
435 Private slot to handle the buttonBox clicked signal. |
|
436 |
|
437 @param button reference to the button that was clicked (QAbstractButton) |
|
438 """ |
|
439 if button == self.okButton: |
|
440 self.__generateCode() |
|
441 self.accept() |