eric6/Project/CreateDialogCodeDialog.py

changeset 6942
2602857055c5
parent 6661
19fcbb7be3da
child 7050
b66cac9a6560
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 to generate code for a Qt4/Qt5 dialog.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode # __IGNORE_EXCEPTION__
13 except NameError:
14 pass
15
16 import sys
17 import os
18 import json
19
20 from PyQt5.QtCore import pyqtSlot, Qt, QMetaObject, QRegExp, \
21 QSortFilterProxyModel, QProcess, QProcessEnvironment
22 from PyQt5.QtGui import QStandardItemModel, QBrush, QStandardItem
23 from PyQt5.QtWidgets import QDialog, QDialogButtonBox
24
25
26 from E5Gui.E5Application import e5App
27 from E5Gui import E5MessageBox
28
29 from .Ui_CreateDialogCodeDialog import Ui_CreateDialogCodeDialog
30 from .NewDialogClassDialog import NewDialogClassDialog
31
32 from eric6config import getConfig
33
34
35 pyqtSignatureRole = Qt.UserRole + 1
36 pythonSignatureRole = Qt.UserRole + 2
37 rubySignatureRole = Qt.UserRole + 3
38 returnTypeRole = Qt.UserRole + 4
39 parameterTypesListRole = Qt.UserRole + 5
40 parameterNamesListRole = Qt.UserRole + 6
41
42
43 class CreateDialogCodeDialog(QDialog, Ui_CreateDialogCodeDialog):
44 """
45 Class implementing a dialog to generate code for a Qt4/Qt5 dialog.
46 """
47 DialogClasses = {
48 "QDialog", "QWidget", "QMainWindow", "QWizard", "QWizardPage",
49 "QDockWidget", "QFrame", "QGroupBox", "QScrollArea", "QMdiArea",
50 "QTabWidget", "QToolBox", "QStackedWidget"
51 }
52 Separator = 25 * "="
53
54 def __init__(self, formName, project, parent=None):
55 """
56 Constructor
57
58 @param formName name of the file containing the form (string)
59 @param project reference to the project object
60 @param parent parent widget if the dialog (QWidget)
61 """
62 super(CreateDialogCodeDialog, self).__init__(parent)
63 self.setupUi(self)
64
65 self.okButton = self.buttonBox.button(QDialogButtonBox.Ok)
66
67 self.slotsView.header().hide()
68
69 self.project = project
70
71 self.formFile = formName
72 filename, ext = os.path.splitext(self.formFile)
73 self.srcFile = '{0}{1}'.format(
74 filename, self.project.getDefaultSourceExtension())
75
76 self.slotsModel = QStandardItemModel()
77 self.proxyModel = QSortFilterProxyModel()
78 self.proxyModel.setDynamicSortFilter(True)
79 self.proxyModel.setSourceModel(self.slotsModel)
80 self.slotsView.setModel(self.proxyModel)
81
82 # initialize some member variables
83 self.__initError = False
84 self.__module = None
85
86 if os.path.exists(self.srcFile):
87 vm = e5App().getObject("ViewManager")
88 ed = vm.getOpenEditor(self.srcFile)
89 if ed and not vm.checkDirty(ed):
90 self.__initError = True
91 return
92
93 try:
94 splitExt = os.path.splitext(self.srcFile)
95 if len(splitExt) == 2:
96 exts = [splitExt[1]]
97 else:
98 exts = None
99 from Utilities import ModuleParser
100 self.__module = ModuleParser.readModule(
101 self.srcFile, extensions=exts, caching=False)
102 except ImportError:
103 pass
104
105 if self.__module is not None:
106 self.filenameEdit.setText(self.srcFile)
107
108 classesList = []
109 vagueClassesList = []
110 for cls in list(self.__module.classes.values()):
111 if not set(cls.super).isdisjoint(
112 CreateDialogCodeDialog.DialogClasses):
113 classesList.append(cls.name)
114 else:
115 vagueClassesList.append(cls.name)
116 classesList.sort()
117 self.classNameCombo.addItems(classesList)
118 if vagueClassesList:
119 if classesList:
120 self.classNameCombo.addItem(
121 CreateDialogCodeDialog.Separator)
122 self.classNameCombo.addItems(sorted(vagueClassesList))
123
124 if os.path.exists(self.srcFile) and \
125 self.__module is not None and \
126 self.classNameCombo.count() == 0:
127 self.__initError = True
128 E5MessageBox.critical(
129 self,
130 self.tr("Create Dialog Code"),
131 self.tr(
132 """The file <b>{0}</b> exists but does not contain"""
133 """ any classes.""").format(self.srcFile))
134
135 self.okButton.setEnabled(self.classNameCombo.count() > 0)
136
137 self.__updateSlotsModel()
138
139 def initError(self):
140 """
141 Public method to determine, if there was an initialzation error.
142
143 @return flag indicating an initialzation error (boolean)
144 """
145 return self.__initError
146
147 def __runUicLoadUi(self, command):
148 """
149 Private method to run the UicLoadUi.py script with the given command
150 and return the output.
151
152 @param command uic command to be run
153 @type str
154 @return tuple of process output and error flag
155 @rtype tuple of (str, bool)
156 """
157 venvName = self.project.getDebugProperty("VIRTUALENV")
158 venvManager = e5App().getObject("VirtualEnvManager")
159 interpreter = venvManager.getVirtualenvInterpreter(venvName)
160 execPath = venvManager.getVirtualenvExecPath(venvName)
161
162 if not interpreter:
163 interpreter = sys.executable
164
165 env = QProcessEnvironment.systemEnvironment()
166 if execPath:
167 if env.contains("PATH"):
168 env.insert(
169 "PATH", os.pathsep.join([execPath, env.value("PATH")])
170 )
171 else:
172 env.insert("PATH", execPath)
173
174 loadUi = os.path.join(os.path.dirname(__file__), "UicLoadUi.py")
175 args = [
176 loadUi,
177 command,
178 self.formFile,
179 self.project.getProjectPath(),
180 ]
181
182 uicText = ""
183 ok = False
184
185 proc = QProcess()
186 proc.setWorkingDirectory(self.project.getProjectPath())
187 proc.setProcessEnvironment(env)
188 proc.start(interpreter, args)
189 started = proc.waitForStarted(5000)
190 finished = proc.waitForFinished(30000)
191 if started and finished:
192 output = proc.readAllStandardOutput()
193 outText = str(output, "utf-8", "replace")
194 if proc.exitCode() == 0:
195 ok = True
196 uicText = outText.strip()
197 else:
198 E5MessageBox.critical(
199 self,
200 self.tr("uic error"),
201 self.tr(
202 """<p>There was an error loading the form <b>{0}</b>"""
203 """.</p><p>{1}</p>""").format(
204 self.formFile, outText)
205 )
206 else:
207 E5MessageBox.critical(
208 self,
209 self.tr("uic error"),
210 self.tr(
211 """<p>The project specific Python interpreter <b>{0}</b>"""
212 """ could not be started or did not finish within 30"""
213 """ seconds.</p>""").format(interpreter)
214 )
215
216 return uicText, ok
217
218 def __objectName(self):
219 """
220 Private method to get the object name of a form.
221
222 @return object name
223 @rtype str
224 """
225 objectName = ""
226
227 output, ok = self.__runUicLoadUi("object_name")
228 if ok and output:
229 objectName = output
230
231 return objectName
232
233 def __className(self):
234 """
235 Private method to get the class name of a form.
236
237 @return class name
238 @rtype str
239 """
240 className = ""
241
242 output, ok = self.__runUicLoadUi("class_name")
243 if ok and output:
244 className = output
245
246 return className
247
248 def __signatures(self):
249 """
250 Private slot to get the signatures.
251
252 @return list of signatures (list of strings)
253 """
254 if self.__module is None:
255 return []
256
257 signatures = []
258 clsName = self.classNameCombo.currentText()
259 if clsName:
260 cls = self.__module.classes[clsName]
261 for meth in list(cls.methods.values()):
262 if meth.name.startswith("on_"):
263 if meth.pyqtSignature is not None:
264 sig = ", ".join(
265 [bytes(QMetaObject.normalizedType(t)).decode()
266 for t in meth.pyqtSignature.split(",")])
267 signatures.append("{0}({1})".format(meth.name, sig))
268 else:
269 signatures.append(meth.name)
270 return signatures
271
272 def __mapType(self, type_):
273 """
274 Private method to map a type as reported by Qt's meta object to the
275 correct Python type.
276
277 @param type_ type as reported by Qt (QByteArray)
278 @return mapped Python type (string)
279 """
280 mapped = bytes(type_).decode()
281
282 # I. always check for *
283 mapped = mapped.replace("*", "")
284
285 if self.project.getProjectLanguage() != "Python2" or \
286 self.project.getProjectType in ("PySide", "PySide2"):
287 # 1. check for const
288 mapped = mapped.replace("const ", "")
289
290 # 2. replace QString and QStringList
291 mapped = mapped.replace("QStringList", "list")\
292 .replace("QString", "str")
293
294 # 3. replace double by float
295 mapped = mapped.replace("double", "float")
296
297 return mapped
298
299 def __updateSlotsModel(self):
300 """
301 Private slot to update the slots tree display.
302 """
303 self.filterEdit.clear()
304
305 output, ok = self.__runUicLoadUi("signatures")
306 if ok and output:
307 objectsList = json.loads(output.strip())
308
309 signatureList = self.__signatures()
310
311 self.slotsModel.clear()
312 self.slotsModel.setHorizontalHeaderLabels([""])
313 for objectDict in objectsList:
314 itm = QStandardItem("{0} ({1})".format(
315 objectDict["name"],
316 objectDict["class_name"]))
317 self.slotsModel.appendRow(itm)
318 for methodDict in objectDict["methods"]:
319 itm2 = QStandardItem(methodDict["signature"])
320 itm.appendRow(itm2)
321
322 if self.__module is not None:
323 if methodDict["methods"][0] in signatureList or \
324 methodDict["methods"][1] in signatureList:
325 itm2.setFlags(
326 Qt.ItemFlags(Qt.ItemIsEnabled))
327 itm2.setCheckState(Qt.Checked)
328 itm2.setForeground(QBrush(Qt.blue))
329 continue
330
331 itm2.setData(methodDict["pyqt_signature"],
332 pyqtSignatureRole)
333 itm2.setData(methodDict["python_signature"],
334 pythonSignatureRole)
335 itm2.setData(methodDict["return_type"],
336 returnTypeRole)
337 itm2.setData(methodDict["parameter_types"],
338 parameterTypesListRole)
339 itm2.setData(methodDict["parameter_names"],
340 parameterNamesListRole)
341
342 itm2.setFlags(Qt.ItemFlags(
343 Qt.ItemIsUserCheckable |
344 Qt.ItemIsEnabled |
345 Qt.ItemIsSelectable)
346 )
347 itm2.setCheckState(Qt.Unchecked)
348
349 self.slotsView.sortByColumn(0, Qt.AscendingOrder)
350
351 def __generateCode(self):
352 """
353 Private slot to generate the code as requested by the user.
354 """
355 # first decide on extension
356 if self.filenameEdit.text().endswith(".py") or \
357 self.filenameEdit.text().endswith(".pyw"):
358 self.__generatePythonCode()
359 elif self.filenameEdit.text().endswith(".rb"):
360 pass
361 # second decide on project language
362 elif self.project.getProjectLanguage() in ["Python2", "Python3"]:
363 self.__generatePythonCode()
364 elif self.project.getProjectLanguage() == "Ruby":
365 pass
366 else:
367 # assume Python (our global default)
368 self.__generatePythonCode()
369
370 def __generatePythonCode(self):
371 """
372 Private slot to generate Python code as requested by the user.
373 """
374 # init some variables
375 sourceImpl = []
376 appendAtIndex = -1
377 indentStr = " "
378 slotsCode = []
379
380 if self.__module is None:
381 # new file
382 try:
383 if self.project.getProjectLanguage() == "Python2":
384 if self.project.getProjectType() == "PySide":
385 tmplName = os.path.join(
386 getConfig('ericCodeTemplatesDir'),
387 "impl_pyside.py2.tmpl")
388 elif self.project.getProjectType() == "PySide2":
389 tmplName = os.path.join(
390 getConfig('ericCodeTemplatesDir'),
391 "impl_pyside2.py2.tmpl")
392 elif self.project.getProjectType() == "PyQt5":
393 tmplName = os.path.join(
394 getConfig('ericCodeTemplatesDir'),
395 "impl_pyqt5.py2.tmpl")
396 else:
397 tmplName = os.path.join(
398 getConfig('ericCodeTemplatesDir'),
399 "impl_pyqt.py2.tmpl")
400 else:
401 if self.project.getProjectType() == "PySide":
402 tmplName = os.path.join(
403 getConfig('ericCodeTemplatesDir'),
404 "impl_pyside.py.tmpl")
405 elif self.project.getProjectType() == "PySide2":
406 tmplName = os.path.join(
407 getConfig('ericCodeTemplatesDir'),
408 "impl_pyside2.py.tmpl")
409 elif self.project.getProjectType() in [
410 "PyQt5", "E6Plugin"]:
411 tmplName = os.path.join(
412 getConfig('ericCodeTemplatesDir'),
413 "impl_pyqt5.py.tmpl")
414 else:
415 tmplName = os.path.join(
416 getConfig('ericCodeTemplatesDir'),
417 "impl_pyqt.py.tmpl")
418 tmplFile = open(tmplName, 'r', encoding="utf-8")
419 template = tmplFile.read()
420 tmplFile.close()
421 except IOError as why:
422 E5MessageBox.critical(
423 self,
424 self.tr("Code Generation"),
425 self.tr(
426 """<p>Could not open the code template file"""
427 """ "{0}".</p><p>Reason: {1}</p>""")
428 .format(tmplName, str(why)))
429 return
430
431 objName = self.__objectName()
432 if objName:
433 template = template\
434 .replace(
435 "$FORMFILE$",
436 os.path.splitext(os.path.basename(self.formFile))[0])\
437 .replace("$FORMCLASS$", objName)\
438 .replace("$CLASSNAME$", self.classNameCombo.currentText())\
439 .replace("$SUPERCLASS$", self.__className())
440
441 sourceImpl = template.splitlines(True)
442 appendAtIndex = -1
443
444 # determine indent string
445 for line in sourceImpl:
446 if line.lstrip().startswith("def __init__"):
447 indentStr = line.replace(line.lstrip(), "")
448 break
449 else:
450 # extend existing file
451 try:
452 srcFile = open(self.srcFile, 'r', encoding="utf-8")
453 sourceImpl = srcFile.readlines()
454 srcFile.close()
455 if not sourceImpl[-1].endswith("\n"):
456 sourceImpl[-1] = "{0}{1}".format(sourceImpl[-1], "\n")
457 except IOError as why:
458 E5MessageBox.critical(
459 self,
460 self.tr("Code Generation"),
461 self.tr(
462 """<p>Could not open the source file "{0}".</p>"""
463 """<p>Reason: {1}</p>""")
464 .format(self.srcFile, str(why)))
465 return
466
467 cls = self.__module.classes[self.classNameCombo.currentText()]
468 if cls.endlineno == len(sourceImpl) or cls.endlineno == -1:
469 appendAtIndex = -1
470 # delete empty lines at end
471 while not sourceImpl[-1].strip():
472 del sourceImpl[-1]
473 else:
474 appendAtIndex = cls.endlineno - 1
475 while not sourceImpl[appendAtIndex].strip():
476 appendAtIndex -= 1
477 appendAtIndex += 1
478
479 # determine indent string
480 for line in sourceImpl[cls.lineno:cls.endlineno + 1]:
481 if line.lstrip().startswith("def __init__"):
482 indentStr = line.replace(line.lstrip(), "")
483 break
484
485 # do the coding stuff
486 if self.project.getProjectLanguage() == "Python2":
487 if self.project.getProjectType() in ("PySide", "PySide2"):
488 pyqtSignatureFormat = '@Slot({0})'
489 elif self.project.getProjectType() == "PyQt5":
490 pyqtSignatureFormat = '@pyqtSlot({0})'
491 else:
492 pyqtSignatureFormat = '@pyqtSignature("{0}")'
493 else:
494 if self.project.getProjectType() in ("PySide", "PySide2"):
495 pyqtSignatureFormat = '@Slot({0})'
496 else:
497 pyqtSignatureFormat = '@pyqtSlot({0})'
498 for row in range(self.slotsModel.rowCount()):
499 topItem = self.slotsModel.item(row)
500 for childRow in range(topItem.rowCount()):
501 child = topItem.child(childRow)
502 if child.checkState() and \
503 child.flags() & Qt.ItemFlags(Qt.ItemIsUserCheckable):
504 slotsCode.append('{0}\n'.format(indentStr))
505 slotsCode.append('{0}{1}\n'.format(
506 indentStr,
507 pyqtSignatureFormat.format(
508 child.data(pyqtSignatureRole))))
509 slotsCode.append('{0}def {1}:\n'.format(
510 indentStr, child.data(pythonSignatureRole)))
511 indentStr2 = indentStr * 2
512 slotsCode.append('{0}"""\n'.format(indentStr2))
513 slotsCode.append(
514 '{0}Slot documentation goes here.\n'.format(
515 indentStr2))
516 if child.data(returnTypeRole) or \
517 child.data(parameterTypesListRole):
518 slotsCode.append('{0}\n'.format(indentStr2))
519 if child.data(parameterTypesListRole):
520 for name, type_ in zip(
521 child.data(parameterNamesListRole),
522 child.data(parameterTypesListRole)):
523 slotsCode.append(
524 '{0}@param {1} DESCRIPTION\n'.format(
525 indentStr2, name))
526 slotsCode.append('{0}@type {1}\n'.format(
527 indentStr2, type_))
528 if child.data(returnTypeRole):
529 slotsCode.append(
530 '{0}@returns DESCRIPTION\n'.format(
531 indentStr2))
532 slotsCode.append('{0}@rtype {1}\n'.format(
533 indentStr2, child.data(returnTypeRole)))
534 slotsCode.append('{0}"""\n'.format(indentStr2))
535 slotsCode.append('{0}# {1}: not implemented yet\n'.format(
536 indentStr2, "TODO"))
537 slotsCode.append('{0}raise NotImplementedError\n'.format(
538 indentStr2))
539
540 if appendAtIndex == -1:
541 sourceImpl.extend(slotsCode)
542 else:
543 sourceImpl[appendAtIndex:appendAtIndex] = slotsCode
544
545 # write the new code
546 try:
547 if self.project.useSystemEol():
548 newline = None
549 else:
550 newline = self.project.getEolString()
551 srcFile = open(self.filenameEdit.text(), 'w', encoding="utf-8",
552 newline=newline)
553 srcFile.write("".join(sourceImpl))
554 srcFile.close()
555 except IOError as why:
556 E5MessageBox.critical(
557 self,
558 self.tr("Code Generation"),
559 self.tr("""<p>Could not write the source file "{0}".</p>"""
560 """<p>Reason: {1}</p>""")
561 .format(self.filenameEdit.text(), str(why)))
562 return
563
564 self.project.appendFile(self.filenameEdit.text())
565
566 @pyqtSlot(int)
567 def on_classNameCombo_activated(self, index):
568 """
569 Private slot to handle the activated signal of the classname combo.
570
571 @param index index of the activated item (integer)
572 """
573 if (self.classNameCombo.currentText() ==
574 CreateDialogCodeDialog.Separator):
575 self.okButton.setEnabled(False)
576 self.filterEdit.clear()
577 self.slotsModel.clear()
578 self.slotsModel.setHorizontalHeaderLabels([""])
579 else:
580 self.okButton.setEnabled(True)
581 self.__updateSlotsModel()
582
583 def on_filterEdit_textChanged(self, text):
584 """
585 Private slot called, when thext of the filter edit has changed.
586
587 @param text changed text (string)
588 """
589 re = QRegExp(text, Qt.CaseInsensitive, QRegExp.RegExp2)
590 self.proxyModel.setFilterRegExp(re)
591
592 @pyqtSlot()
593 def on_newButton_clicked(self):
594 """
595 Private slot called to enter the data for a new dialog class.
596 """
597 path, file = os.path.split(self.srcFile)
598 objName = self.__objectName()
599 if objName:
600 dlg = NewDialogClassDialog(objName, file, path, self)
601 if dlg.exec_() == QDialog.Accepted:
602 className, fileName = dlg.getData()
603
604 self.classNameCombo.clear()
605 self.classNameCombo.addItem(className)
606 self.srcFile = fileName
607 self.filenameEdit.setText(self.srcFile)
608 self.__module = None
609
610 self.okButton.setEnabled(self.classNameCombo.count() > 0)
611
612 def on_buttonBox_clicked(self, button):
613 """
614 Private slot to handle the buttonBox clicked signal.
615
616 @param button reference to the button that was clicked
617 (QAbstractButton)
618 """
619 if button == self.okButton:
620 self.__generateCode()
621 self.accept()

eric ide

mercurial