eric7/Project/CreateDialogCodeDialog.py

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

eric ide

mercurial