eric7/Project/ProjectFormsBrowser.py

branch
eric7
changeset 8312
800c432b34c8
parent 8265
0090cfa83159
child 8314
e3642a6a1e71
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a class used to display the forms part of the project.
8 """
9
10 import os
11 import sys
12 import shutil
13 import contextlib
14
15 from PyQt5.QtCore import QThread, QFileInfo, pyqtSignal, QProcess
16 from PyQt5.QtWidgets import QDialog, QInputDialog, QApplication, QMenu
17
18 from E5Gui.E5Application import e5App
19 from E5Gui import E5MessageBox, E5FileDialog
20 from E5Gui.E5ProgressDialog import E5ProgressDialog
21
22 from .ProjectBrowserModel import (
23 ProjectBrowserFileItem, ProjectBrowserSimpleDirectoryItem,
24 ProjectBrowserDirectoryItem, ProjectBrowserFormType
25 )
26 from .ProjectBaseBrowser import ProjectBaseBrowser
27
28 import UI.PixmapCache
29 from UI.NotificationWidget import NotificationTypes
30
31
32 import Preferences
33 import Utilities
34
35 from eric6config import getConfig
36
37
38 class ProjectFormsBrowser(ProjectBaseBrowser):
39 """
40 A class used to display the forms part of the project.
41
42 @signal appendStderr(str) emitted after something was received from
43 a QProcess on stderr
44 @signal uipreview(str) emitted to preview a forms file
45 @signal showMenu(str, QMenu) emitted when a menu is about to be shown. The
46 name of the menu and a reference to the menu are given.
47 @signal menusAboutToBeCreated() emitted when the context menus are about to
48 be created. This is the right moment to add or remove hook methods.
49 """
50 appendStderr = pyqtSignal(str)
51 uipreview = pyqtSignal(str)
52 showMenu = pyqtSignal(str, QMenu)
53 menusAboutToBeCreated = pyqtSignal()
54
55 Pyuic5IndentDefault = 4
56 Pyuic6IndentDefault = 4
57
58 def __init__(self, project, parent=None):
59 """
60 Constructor
61
62 @param project reference to the project object
63 @param parent parent widget of this browser (QWidget)
64 """
65 ProjectBaseBrowser.__init__(self, project, ProjectBrowserFormType,
66 parent)
67
68 self.selectedItemsFilter = [ProjectBrowserFileItem,
69 ProjectBrowserSimpleDirectoryItem]
70
71 self.setWindowTitle(self.tr('Forms'))
72
73 self.setWhatsThis(self.tr(
74 """<b>Project Forms Browser</b>"""
75 """<p>This allows to easily see all forms contained in the"""
76 """ current project. Several actions can be executed via the"""
77 """ context menu.</p>"""
78 ))
79
80 # templates for Qt
81 # these two lists have to stay in sync
82 self.templates4 = [
83 'dialog4.tmpl', 'widget4.tmpl', 'mainwindow4.tmpl',
84 'dialogbuttonboxbottom4.tmpl', 'dialogbuttonboxright4.tmpl',
85 'dialogbuttonsbottom4.tmpl', 'dialogbuttonsbottomcenter4.tmpl',
86 'dialogbuttonsright4.tmpl', '', 'wizard4.tmpl', 'wizardpage4.tmpl',
87 'qdockwidget4.tmpl', 'qframe4.tmpl', 'qgroupbox4.tmpl',
88 'qscrollarea4.tmpl', 'qmdiarea4.tmpl', 'qtabwidget4.tmpl',
89 'qtoolbox4.tmpl', 'qstackedwidget4.tmpl'
90 ]
91 self.templateTypes4 = [
92 self.tr("Dialog"),
93 self.tr("Widget"),
94 self.tr("Main Window"),
95 self.tr("Dialog with Buttonbox (Bottom)"),
96 self.tr("Dialog with Buttonbox (Right)"),
97 self.tr("Dialog with Buttons (Bottom)"),
98 self.tr("Dialog with Buttons (Bottom-Center)"),
99 self.tr("Dialog with Buttons (Right)"),
100 '',
101 self.tr("QWizard"),
102 self.tr("QWizardPage"),
103 self.tr("QDockWidget"),
104 self.tr("QFrame"),
105 self.tr("QGroupBox"),
106 self.tr("QScrollArea"),
107 self.tr("QMdiArea"),
108 self.tr("QTabWidget"),
109 self.tr("QToolBox"),
110 self.tr("QStackedWidget"),
111 ]
112
113 self.compileProc = None
114 self.__uicompiler = ""
115
116 self.project.projectClosed.connect(self.__resetUiCompiler)
117 self.project.projectPropertiesChanged.connect(self.__resetUiCompiler)
118
119 def _createPopupMenus(self):
120 """
121 Protected overloaded method to generate the popup menu.
122 """
123 self.menuActions = []
124 self.multiMenuActions = []
125 self.dirMenuActions = []
126 self.dirMultiMenuActions = []
127
128 self.menusAboutToBeCreated.emit()
129
130 projectType = self.project.getProjectType()
131
132 self.menu = QMenu(self)
133 if projectType in ["PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"]:
134 self.menu.addAction(
135 self.tr('Compile form'), self.__compileForm)
136 self.menu.addAction(
137 self.tr('Compile all forms'),
138 self.__compileAllForms)
139 self.menu.addAction(
140 self.tr('Generate Dialog Code...'),
141 self.__generateDialogCode)
142 self.menu.addSeparator()
143 self.__pyuicConfigAct = self.menu.addAction(
144 self.tr('Configure uic Compiler'),
145 self.__configureUicCompiler)
146 self.menu.addSeparator()
147 self.menu.addAction(
148 self.tr('Open in Qt-Designer'), self.__openFile)
149 self.menu.addAction(
150 self.tr('Open in Editor'), self.__openFileInEditor)
151 self.menu.addSeparator()
152 self.menu.addAction(self.tr('Preview form'), self.__UIPreview)
153 self.menu.addAction(
154 self.tr('Preview translations'), self.__TRPreview)
155 else:
156 if self.hooks["compileForm"] is not None:
157 self.menu.addAction(
158 self.hooksMenuEntries.get(
159 "compileForm",
160 self.tr('Compile form')), self.__compileForm)
161 if self.hooks["compileAllForms"] is not None:
162 self.menu.addAction(
163 self.hooksMenuEntries.get(
164 "compileAllForms",
165 self.tr('Compile all forms')),
166 self.__compileAllForms)
167 if self.hooks["generateDialogCode"] is not None:
168 self.menu.addAction(
169 self.hooksMenuEntries.get(
170 "generateDialogCode",
171 self.tr('Generate Dialog Code...')),
172 self.__generateDialogCode)
173 if (
174 self.hooks["compileForm"] is not None or
175 self.hooks["compileAllForms"] is not None or
176 self.hooks["generateDialogCode"] is not None
177 ):
178 self.menu.addSeparator()
179 if self.hooks["open"] is not None:
180 self.menu.addAction(
181 self.hooksMenuEntries.get("open", self.tr('Open')),
182 self.__openFile)
183 self.menu.addAction(self.tr('Open'), self.__openFileInEditor)
184 self.menu.addSeparator()
185 act = self.menu.addAction(self.tr('Rename file'), self._renameFile)
186 self.menuActions.append(act)
187 act = self.menu.addAction(
188 self.tr('Remove from project'), self._removeFile)
189 self.menuActions.append(act)
190 act = self.menu.addAction(self.tr('Delete'), self.__deleteFile)
191 self.menuActions.append(act)
192 self.menu.addSeparator()
193 if projectType in ["PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"]:
194 self.menu.addAction(self.tr('New form...'), self.__newForm)
195 else:
196 if self.hooks["newForm"] is not None:
197 self.menu.addAction(
198 self.hooksMenuEntries.get(
199 "newForm", self.tr('New form...')), self.__newForm)
200 self.menu.addAction(self.tr('Add forms...'), self.__addFormFiles)
201 self.menu.addAction(
202 self.tr('Add forms directory...'), self.__addFormsDirectory)
203 self.menu.addSeparator()
204 self.menu.addAction(
205 self.tr('Copy Path to Clipboard'), self._copyToClipboard)
206 self.menu.addSeparator()
207 self.menu.addAction(
208 self.tr('Expand all directories'), self._expandAllDirs)
209 self.menu.addAction(
210 self.tr('Collapse all directories'), self._collapseAllDirs)
211 self.menu.addSeparator()
212 self.menu.addAction(self.tr('Configure...'), self._configure)
213
214 self.backMenu = QMenu(self)
215 if (
216 projectType in [
217 "PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"
218 ] or self.hooks["compileAllForms"] is not None
219 ):
220 self.backMenu.addAction(
221 self.tr('Compile all forms'), self.__compileAllForms)
222 self.backMenu.addSeparator()
223 self.__pyuicBackConfigAct = self.backMenu.addAction(
224 self.tr('Configure uic Compiler'),
225 self.__configureUicCompiler)
226 self.backMenu.addSeparator()
227 self.backMenu.addAction(self.tr('New form...'), self.__newForm)
228 else:
229 if self.hooks["newForm"] is not None:
230 self.backMenu.addAction(
231 self.hooksMenuEntries.get(
232 "newForm", self.tr('New form...')), self.__newForm)
233 self.backMenu.addAction(
234 self.tr('Add forms...'), self.project.addUiFiles)
235 self.backMenu.addAction(
236 self.tr('Add forms directory...'), self.project.addUiDir)
237 self.backMenu.addSeparator()
238 self.backMenu.addAction(
239 self.tr('Expand all directories'), self._expandAllDirs)
240 self.backMenu.addAction(
241 self.tr('Collapse all directories'), self._collapseAllDirs)
242 self.backMenu.addSeparator()
243 self.backMenu.addAction(self.tr('Configure...'), self._configure)
244 self.backMenu.setEnabled(False)
245
246 # create the menu for multiple selected files
247 self.multiMenu = QMenu(self)
248 if projectType in ["PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"]:
249 self.multiMenu.addAction(
250 self.tr('Compile forms'), self.__compileSelectedForms)
251 self.multiMenu.addSeparator()
252 self.__pyuicMultiConfigAct = self.multiMenu.addAction(
253 self.tr('Configure uic Compiler'),
254 self.__configureUicCompiler)
255 self.multiMenu.addSeparator()
256 self.multiMenu.addAction(
257 self.tr('Open in Qt-Designer'), self.__openFile)
258 self.multiMenu.addAction(
259 self.tr('Open in Editor'), self.__openFileInEditor)
260 self.multiMenu.addSeparator()
261 self.multiMenu.addAction(
262 self.tr('Preview translations'), self.__TRPreview)
263 else:
264 if self.hooks["compileSelectedForms"] is not None:
265 act = self.multiMenu.addAction(
266 self.hooksMenuEntries.get(
267 "compileSelectedForms",
268 self.tr('Compile forms')),
269 self.__compileSelectedForms)
270 self.multiMenu.addSeparator()
271 if self.hooks["open"] is not None:
272 self.multiMenu.addAction(
273 self.hooksMenuEntries.get("open", self.tr('Open')),
274 self.__openFile)
275 self.multiMenu.addAction(
276 self.tr('Open'), self.__openFileInEditor)
277 self.multiMenu.addSeparator()
278 act = self.multiMenu.addAction(
279 self.tr('Remove from project'), self._removeFile)
280 self.multiMenuActions.append(act)
281 act = self.multiMenu.addAction(
282 self.tr('Delete'), self.__deleteFile)
283 self.multiMenuActions.append(act)
284 self.multiMenu.addSeparator()
285 self.multiMenu.addAction(
286 self.tr('Expand all directories'), self._expandAllDirs)
287 self.multiMenu.addAction(
288 self.tr('Collapse all directories'), self._collapseAllDirs)
289 self.multiMenu.addSeparator()
290 self.multiMenu.addAction(self.tr('Configure...'), self._configure)
291
292 self.dirMenu = QMenu(self)
293 if projectType in ["PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"]:
294 self.dirMenu.addAction(
295 self.tr('Compile all forms'), self.__compileAllForms)
296 self.dirMenu.addSeparator()
297 self.__pyuicDirConfigAct = self.dirMenu.addAction(
298 self.tr('Configure uic Compiler'),
299 self.__configureUicCompiler)
300 self.dirMenu.addSeparator()
301 else:
302 if self.hooks["compileAllForms"] is not None:
303 self.dirMenu.addAction(
304 self.hooksMenuEntries.get(
305 "compileAllForms",
306 self.tr('Compile all forms')),
307 self.__compileAllForms)
308 self.dirMenu.addSeparator()
309 act = self.dirMenu.addAction(
310 self.tr('Remove from project'), self._removeDir)
311 self.dirMenuActions.append(act)
312 act = self.dirMenu.addAction(
313 self.tr('Delete'), self._deleteDirectory)
314 self.dirMenuActions.append(act)
315 self.dirMenu.addSeparator()
316 if projectType in ["PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"]:
317 self.dirMenu.addAction(self.tr('New form...'), self.__newForm)
318 else:
319 if self.hooks["newForm"] is not None:
320 self.dirMenu.addAction(
321 self.hooksMenuEntries.get(
322 "newForm",
323 self.tr('New form...')), self.__newForm)
324 self.dirMenu.addAction(
325 self.tr('Add forms...'), self.__addFormFiles)
326 self.dirMenu.addAction(
327 self.tr('Add forms directory...'), self.__addFormsDirectory)
328 self.dirMenu.addSeparator()
329 self.dirMenu.addAction(
330 self.tr('Copy Path to Clipboard'), self._copyToClipboard)
331 self.dirMenu.addSeparator()
332 self.dirMenu.addAction(
333 self.tr('Expand all directories'), self._expandAllDirs)
334 self.dirMenu.addAction(
335 self.tr('Collapse all directories'), self._collapseAllDirs)
336 self.dirMenu.addSeparator()
337 self.dirMenu.addAction(self.tr('Configure...'), self._configure)
338
339 self.dirMultiMenu = QMenu(self)
340 if projectType in ["PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"]:
341 self.dirMultiMenu.addAction(
342 self.tr('Compile all forms'), self.__compileAllForms)
343 self.dirMultiMenu.addSeparator()
344 self.__pyuicDirMultiConfigAct = self.dirMultiMenu.addAction(
345 self.tr('Configure uic Compiler'),
346 self.__configureUicCompiler)
347 self.dirMultiMenu.addSeparator()
348 else:
349 if self.hooks["compileAllForms"] is not None:
350 self.dirMultiMenu.addAction(
351 self.hooksMenuEntries.get(
352 "compileAllForms",
353 self.tr('Compile all forms')),
354 self.__compileAllForms)
355 self.dirMultiMenu.addSeparator()
356 self.dirMultiMenu.addAction(
357 self.tr('Add forms...'), self.project.addUiFiles)
358 self.dirMultiMenu.addAction(
359 self.tr('Add forms directory...'), self.project.addUiDir)
360 self.dirMultiMenu.addSeparator()
361 self.dirMultiMenu.addAction(
362 self.tr('Expand all directories'), self._expandAllDirs)
363 self.dirMultiMenu.addAction(
364 self.tr('Collapse all directories'), self._collapseAllDirs)
365 self.dirMultiMenu.addSeparator()
366 self.dirMultiMenu.addAction(
367 self.tr('Configure...'), self._configure)
368
369 self.menu.aboutToShow.connect(self.__showContextMenu)
370 self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
371 self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
372 self.dirMultiMenu.aboutToShow.connect(self.__showContextMenuDirMulti)
373 self.backMenu.aboutToShow.connect(self.__showContextMenuBack)
374 self.mainMenu = self.menu
375
376 def _contextMenuRequested(self, coord):
377 """
378 Protected slot to show the context menu.
379
380 @param coord the position of the mouse pointer (QPoint)
381 """
382 if not self.project.isOpen():
383 return
384
385 enable = (
386 self.project.getProjectType() in ("PyQt5", "PyQt6", "E6Plugin")
387 )
388 self.__pyuicConfigAct.setEnabled(enable)
389 self.__pyuicMultiConfigAct.setEnabled(enable)
390 self.__pyuicDirConfigAct.setEnabled(enable)
391 self.__pyuicDirMultiConfigAct.setEnabled(enable)
392 self.__pyuicBackConfigAct.setEnabled(enable)
393
394 with contextlib.suppress(Exception):
395 categories = self.getSelectedItemsCountCategorized(
396 [ProjectBrowserFileItem, ProjectBrowserSimpleDirectoryItem])
397 cnt = categories["sum"]
398 if cnt <= 1:
399 index = self.indexAt(coord)
400 if index.isValid():
401 self._selectSingleItem(index)
402 categories = self.getSelectedItemsCountCategorized(
403 [ProjectBrowserFileItem,
404 ProjectBrowserSimpleDirectoryItem])
405 cnt = categories["sum"]
406
407 bfcnt = categories[str(ProjectBrowserFileItem)]
408 sdcnt = categories[str(ProjectBrowserSimpleDirectoryItem)]
409 if cnt > 1 and cnt == bfcnt:
410 self.multiMenu.popup(self.mapToGlobal(coord))
411 elif cnt > 1 and cnt == sdcnt:
412 self.dirMultiMenu.popup(self.mapToGlobal(coord))
413 else:
414 index = self.indexAt(coord)
415 if cnt == 1 and index.isValid():
416 if bfcnt == 1:
417 self.menu.popup(self.mapToGlobal(coord))
418 elif sdcnt == 1:
419 self.dirMenu.popup(self.mapToGlobal(coord))
420 else:
421 self.backMenu.popup(self.mapToGlobal(coord))
422 else:
423 self.backMenu.popup(self.mapToGlobal(coord))
424
425 def __showContextMenu(self):
426 """
427 Private slot called by the menu aboutToShow signal.
428 """
429 ProjectBaseBrowser._showContextMenu(self, self.menu)
430
431 self.showMenu.emit("Main", self.menu)
432
433 def __showContextMenuMulti(self):
434 """
435 Private slot called by the multiMenu aboutToShow signal.
436 """
437 ProjectBaseBrowser._showContextMenuMulti(self, self.multiMenu)
438
439 self.showMenu.emit("MainMulti", self.multiMenu)
440
441 def __showContextMenuDir(self):
442 """
443 Private slot called by the dirMenu aboutToShow signal.
444 """
445 ProjectBaseBrowser._showContextMenuDir(self, self.dirMenu)
446
447 self.showMenu.emit("MainDir", self.dirMenu)
448
449 def __showContextMenuDirMulti(self):
450 """
451 Private slot called by the dirMultiMenu aboutToShow signal.
452 """
453 ProjectBaseBrowser._showContextMenuDirMulti(self, self.dirMultiMenu)
454
455 self.showMenu.emit("MainDirMulti", self.dirMultiMenu)
456
457 def __showContextMenuBack(self):
458 """
459 Private slot called by the backMenu aboutToShow signal.
460 """
461 ProjectBaseBrowser._showContextMenuBack(self, self.backMenu)
462
463 self.showMenu.emit("MainBack", self.backMenu)
464
465 def __addFormFiles(self):
466 """
467 Private method to add form files to the project.
468 """
469 itm = self.model().item(self.currentIndex())
470 if isinstance(itm, ProjectBrowserFileItem):
471 dn = os.path.dirname(itm.fileName())
472 elif isinstance(
473 itm,
474 (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem)
475 ):
476 dn = itm.dirName()
477 else:
478 dn = None
479 self.project.addFiles('form', dn)
480
481 def __addFormsDirectory(self):
482 """
483 Private method to add form files of a directory to the project.
484 """
485 itm = self.model().item(self.currentIndex())
486 if isinstance(itm, ProjectBrowserFileItem):
487 dn = os.path.dirname(itm.fileName())
488 elif isinstance(
489 itm,
490 (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem)
491 ):
492 dn = itm.dirName()
493 else:
494 dn = None
495 self.project.addDirectory('form', dn)
496
497 def __openFile(self):
498 """
499 Private slot to handle the Open menu action.
500 """
501 itmList = self.getSelectedItems()
502 for itm in itmList[:]:
503 with contextlib.suppress(Exception):
504 if isinstance(itm, ProjectBrowserFileItem):
505 # hook support
506 if self.hooks["open"] is not None:
507 self.hooks["open"](itm.fileName())
508 else:
509 self.designerFile.emit(itm.fileName())
510
511 def __openFileInEditor(self):
512 """
513 Private slot to handle the Open in Editor menu action.
514 """
515 itmList = self.getSelectedItems()
516 for itm in itmList[:]:
517 self.sourceFile.emit(itm.fileName())
518
519 def _openItem(self):
520 """
521 Protected slot to handle the open popup menu entry.
522 """
523 itmList = self.getSelectedItems()
524 for itm in itmList:
525 if isinstance(itm, ProjectBrowserFileItem):
526 if itm.isDesignerFile():
527 self.designerFile.emit(itm.fileName())
528 else:
529 self.sourceFile.emit(itm.fileName())
530
531 def __UIPreview(self):
532 """
533 Private slot to handle the Preview menu action.
534 """
535 itmList = self.getSelectedItems()
536 self.uipreview.emit(itmList[0].fileName())
537
538 def __TRPreview(self):
539 """
540 Private slot to handle the Preview translations action.
541 """
542 fileNames = []
543 for itm in self.getSelectedItems():
544 fileNames.append(itm.fileName())
545 trfiles = sorted(self.project.pdata["TRANSLATIONS"][:])
546 fileNames.extend([os.path.join(self.project.ppath, trfile)
547 for trfile in trfiles
548 if trfile.endswith('.qm')])
549 self.trpreview[list].emit(fileNames)
550
551 def __newForm(self):
552 """
553 Private slot to handle the New Form menu action.
554 """
555 itm = self.model().item(self.currentIndex())
556 if itm is None:
557 path = self.project.ppath
558 else:
559 try:
560 path = os.path.dirname(itm.fileName())
561 except AttributeError:
562 try:
563 path = itm.dirName()
564 except AttributeError:
565 path = os.path.join(self.project.ppath, itm.data(0))
566
567 if self.hooks["newForm"] is not None:
568 self.hooks["newForm"](path)
569 else:
570 if self.project.getProjectType() in [
571 "PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"
572 ]:
573 self.__newUiForm(path)
574
575 def __newUiForm(self, path):
576 """
577 Private slot to handle the New Form menu action for Qt-related
578 projects.
579
580 @param path full directory path for the new form file (string)
581 """
582 selectedForm, ok = QInputDialog.getItem(
583 None,
584 self.tr("New Form"),
585 self.tr("Select a form type:"),
586 self.templateTypes4,
587 0, False)
588 if not ok or not selectedForm:
589 # user pressed cancel
590 return
591
592 templateIndex = self.templateTypes4.index(selectedForm)
593 templateFile = os.path.join(
594 getConfig('ericTemplatesDir'), self.templates4[templateIndex])
595
596 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
597 self,
598 self.tr("New Form"),
599 path,
600 self.tr("Qt User-Interface Files (*.ui);;All Files (*)"),
601 "",
602 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
603
604 if not fname:
605 # user aborted or didn't enter a filename
606 return
607
608 ext = QFileInfo(fname).suffix()
609 if not ext:
610 ex = selectedFilter.split("(*")[1].split(")")[0]
611 if ex:
612 fname += ex
613
614 if os.path.exists(fname):
615 res = E5MessageBox.yesNo(
616 self,
617 self.tr("New Form"),
618 self.tr("The file already exists! Overwrite it?"),
619 icon=E5MessageBox.Warning)
620 if not res:
621 # user selected to not overwrite
622 return
623
624 try:
625 shutil.copy(templateFile, fname)
626 except OSError as e:
627 E5MessageBox.critical(
628 self,
629 self.tr("New Form"),
630 self.tr(
631 "<p>The new form file <b>{0}</b> could not be created.<br>"
632 "Problem: {1}</p>").format(fname, str(e)))
633 return
634
635 self.project.appendFile(fname)
636 self.designerFile.emit(fname)
637
638 def __deleteFile(self):
639 """
640 Private method to delete a form file from the project.
641 """
642 itmList = self.getSelectedItems()
643
644 files = []
645 fullNames = []
646 for itm in itmList:
647 fn2 = itm.fileName()
648 fullNames.append(fn2)
649 fn = self.project.getRelativePath(fn2)
650 files.append(fn)
651
652 from UI.DeleteFilesConfirmationDialog import (
653 DeleteFilesConfirmationDialog
654 )
655 dlg = DeleteFilesConfirmationDialog(
656 self.parent(),
657 self.tr("Delete forms"),
658 self.tr(
659 "Do you really want to delete these forms from the project?"),
660 files)
661
662 if dlg.exec() == QDialog.DialogCode.Accepted:
663 for fn2, fn in zip(fullNames, files):
664 self.closeSourceWindow.emit(fn2)
665 self.project.deleteFile(fn)
666
667 ###########################################################################
668 ## Methods to handle the various compile commands
669 ###########################################################################
670
671 def __resetUiCompiler(self):
672 """
673 Private slot to reset the determined UI compiler executable.
674 """
675 self.__uicompiler = ""
676
677 def __determineUiCompiler(self):
678 """
679 Private method to determine the UI compiler for the project.
680 """
681 self.__resetUiCompiler()
682
683 if self.project.getProjectLanguage() == "Python3":
684 if self.project.getProjectType() in ["PyQt5", "E6Plugin"]:
685 self.__uicompiler = Utilities.generatePyQtToolPath(
686 'pyuic5', ["py3uic5"])
687 elif self.project.getProjectType() in ["PyQt6"]:
688 self.__uicompiler = Utilities.generatePyQtToolPath(
689 'pyuic6')
690 elif self.project.getProjectType() == "PySide2":
691 self.__uicompiler = Utilities.generatePySideToolPath(
692 'pyside2-uic', variant=2)
693 elif self.project.getProjectType() == "PySide6":
694 self.__uicompiler = Utilities.generatePySideToolPath(
695 'pyside6-uic', variant=6)
696
697 def getUiCompiler(self):
698 """
699 Public method to get the UI compiler executable of the project.
700
701 @return UI compiler executable
702 @rtype str
703 """
704 if not self.__uicompiler:
705 self.__determineUiCompiler()
706
707 return self.__uicompiler
708
709 def __readStdout(self):
710 """
711 Private slot to handle the readyReadStandardOutput signal of the
712 pyuic5/pyuic6/pyside2-uic/pyside6-uic process.
713 """
714 if self.compileProc is None:
715 return
716 self.compileProc.setReadChannel(QProcess.ProcessChannel.StandardOutput)
717
718 while self.compileProc and self.compileProc.canReadLine():
719 self.buf += str(self.compileProc.readLine(),
720 "utf-8", 'replace')
721
722 def __readStderr(self):
723 """
724 Private slot to handle the readyReadStandardError signal of the
725 pyuic5/pyuic6/pyside2-uic/pyside6-uic process.
726 """
727 if self.compileProc is None:
728 return
729
730 ioEncoding = Preferences.getSystem("IOEncoding")
731
732 self.compileProc.setReadChannel(QProcess.ProcessChannel.StandardError)
733 while self.compileProc and self.compileProc.canReadLine():
734 s = self.__uicompiler + ': '
735 error = str(self.compileProc.readLine(),
736 ioEncoding, 'replace')
737 s += error
738 self.appendStderr.emit(s)
739
740 def __compileUIDone(self, exitCode, exitStatus):
741 """
742 Private slot to handle the finished signal of the pyuic/rbuic process.
743
744 @param exitCode exit code of the process (integer)
745 @param exitStatus exit status of the process (QProcess.ExitStatus)
746 """
747 self.compileRunning = False
748 e5App().getObject("ViewManager").enableEditorsCheckFocusIn(True)
749 ui = e5App().getObject("UserInterface")
750 if (
751 exitStatus == QProcess.ExitStatus.NormalExit and
752 exitCode == 0 and
753 self.buf
754 ):
755 ofn = os.path.join(self.project.ppath, self.compiledFile)
756 try:
757 newline = (None if self.project.useSystemEol()
758 else self.project.getEolString())
759 with open(ofn, "w", encoding="utf-8", newline=newline) as f:
760 for line in self.buf.splitlines():
761 f.write(line + "\n")
762 if self.compiledFile not in self.project.pdata["SOURCES"]:
763 self.project.appendFile(ofn)
764 ui.showNotification(
765 UI.PixmapCache.getPixmap("designer48"),
766 self.tr("Form Compilation"),
767 self.tr("The compilation of the form file"
768 " was successful."))
769 self.project.projectFormCompiled.emit(self.compiledFile)
770 except OSError as msg:
771 ui.showNotification(
772 UI.PixmapCache.getPixmap("designer48"),
773 self.tr("Form Compilation"),
774 self.tr(
775 "<p>The compilation of the form file failed.</p>"
776 "<p>Reason: {0}</p>").format(str(msg)),
777 kind=NotificationTypes.CRITICAL,
778 timeout=0)
779 else:
780 ui.showNotification(
781 UI.PixmapCache.getPixmap("designer48"),
782 self.tr("Form Compilation"),
783 self.tr("The compilation of the form file failed."),
784 kind=NotificationTypes.CRITICAL,
785 timeout=0)
786 self.compileProc = None
787
788 def __compileUI(self, fn, noDialog=False, progress=None):
789 """
790 Private method to compile a .ui file to a .py/.rb file.
791
792 @param fn filename of the .ui file to be compiled
793 @param noDialog flag indicating silent operations
794 @param progress reference to the progress dialog
795 @return reference to the compile process (QProcess)
796 """
797 self.compileProc = QProcess()
798 args = []
799 self.buf = ""
800
801 uicompiler = self.getUiCompiler()
802 if not uicompiler:
803 return None
804
805 ofn, ext = os.path.splitext(fn)
806 fn = os.path.join(self.project.ppath, fn)
807
808 if self.project.getProjectLanguage() == "Python3":
809 dirname, filename = os.path.split(ofn)
810 self.compiledFile = os.path.join(dirname, "Ui_" + filename + ".py")
811
812 if self.project.getProjectType() == "PySide2":
813 # PySide2
814 if Preferences.getQt("PySide2FromImports"):
815 args.append("--from-imports")
816 elif self.project.getProjectType() == "PySide6":
817 # PySide6
818 if Preferences.getQt("PySide6FromImports"):
819 args.append("--from-imports")
820 elif self.project.getProjectType() == "PyQt6":
821 # PyQt6
822 if Preferences.getQt("Pyuic6Execute"):
823 args.append("-x")
824 indentWidth = Preferences.getQt("Pyuic6Indent")
825 if indentWidth != self.Pyuic6IndentDefault:
826 args.append("--indent={0}".format(indentWidth))
827 else:
828 # PyQt5
829 if Preferences.getQt("PyuicExecute"):
830 args.append("-x")
831 indentWidth = Preferences.getQt("PyuicIndent")
832 if indentWidth != self.Pyuic5IndentDefault:
833 args.append("--indent={0}".format(indentWidth))
834 if (
835 'uic5' in uicompiler and
836 self.project.pdata["UICPARAMS"]["Package"]
837 ):
838 args.append("--import-from={0}".format(
839 self.project.pdata["UICPARAMS"]["Package"]))
840 elif Preferences.getQt("PyuicFromImports"):
841 args.append("--from-imports")
842 if self.project.pdata["UICPARAMS"]["RcSuffix"]:
843 args.append("--resource-suffix={0}".format(
844 self.project.pdata["UICPARAMS"]["RcSuffix"]))
845 elif self.project.getProjectLanguage() == "Ruby":
846 self.compiledFile = ofn + '.rb'
847 args.append('-x')
848
849 args.append(fn)
850 self.compileProc.finished.connect(self.__compileUIDone)
851 self.compileProc.readyReadStandardOutput.connect(self.__readStdout)
852 self.compileProc.readyReadStandardError.connect(self.__readStderr)
853
854 self.noDialog = noDialog
855 self.compileProc.setWorkingDirectory(self.project.getProjectPath())
856 self.compileProc.start(uicompiler, args)
857 procStarted = self.compileProc.waitForStarted(5000)
858 if procStarted:
859 self.compileRunning = True
860 e5App().getObject("ViewManager").enableEditorsCheckFocusIn(False)
861 return self.compileProc
862 else:
863 self.compileRunning = False
864 if progress is not None:
865 progress.cancel()
866 E5MessageBox.critical(
867 self,
868 self.tr('Process Generation Error'),
869 self.tr(
870 'Could not start {0}.<br>'
871 'Ensure that it is in the search path.'
872 ).format(uicompiler))
873 return None
874
875 def __generateDialogCode(self):
876 """
877 Private method to generate dialog code for the form (Qt only).
878 """
879 itm = self.model().item(self.currentIndex())
880 fn = itm.fileName()
881
882 if self.hooks["generateDialogCode"] is not None:
883 self.hooks["generateDialogCode"](fn)
884 else:
885 from .CreateDialogCodeDialog import CreateDialogCodeDialog
886
887 # change environment
888 sys.path.insert(0, self.project.getProjectPath())
889 cwd = os.getcwd()
890 os.chdir(os.path.dirname(os.path.abspath(fn)))
891
892 dlg = CreateDialogCodeDialog(fn, self.project, self)
893 if not dlg.initError():
894 dlg.exec()
895
896 # reset the environment
897 os.chdir(cwd)
898 del sys.path[0]
899
900 def __compileForm(self):
901 """
902 Private method to compile a form to a source file.
903 """
904 itm = self.model().item(self.currentIndex())
905 fn2 = itm.fileName()
906 fn = self.project.getRelativePath(fn2)
907 if self.hooks["compileForm"] is not None:
908 self.hooks["compileForm"](fn)
909 else:
910 self.__compileUI(fn)
911
912 def __compileAllForms(self):
913 """
914 Private method to compile all forms to source files.
915 """
916 if self.hooks["compileAllForms"] is not None:
917 self.hooks["compileAllForms"](self.project.pdata["FORMS"])
918 else:
919 numForms = len(self.project.pdata["FORMS"])
920 progress = E5ProgressDialog(
921 self.tr("Compiling forms..."),
922 self.tr("Abort"), 0, numForms,
923 self.tr("%v/%m Forms"), self)
924 progress.setModal(True)
925 progress.setMinimumDuration(0)
926 progress.setWindowTitle(self.tr("Forms"))
927
928 for prog, fn in enumerate(self.project.pdata["FORMS"]):
929 progress.setValue(prog)
930 if progress.wasCanceled():
931 break
932
933 proc = self.__compileUI(fn, True, progress)
934 if proc is not None:
935 while proc.state() == QProcess.ProcessState.Running:
936 QApplication.processEvents()
937 QThread.msleep(300)
938 QApplication.processEvents()
939 else:
940 break
941 progress.setValue(numForms)
942
943 def __compileSelectedForms(self):
944 """
945 Private method to compile selected forms to source files.
946 """
947 items = self.getSelectedItems()
948 files = [self.project.getRelativePath(itm.fileName())
949 for itm in items]
950
951 if self.hooks["compileSelectedForms"] is not None:
952 self.hooks["compileSelectedForms"](files)
953 else:
954 numForms = len(files)
955 progress = E5ProgressDialog(
956 self.tr("Compiling forms..."),
957 self.tr("Abort"), 0, numForms,
958 self.tr("%v/%m Forms"), self)
959 progress.setModal(True)
960 progress.setMinimumDuration(0)
961 progress.setWindowTitle(self.tr("Forms"))
962
963 for prog, fn in enumerate(files):
964 progress.setValue(prog)
965 if progress.wasCanceled():
966 break
967
968 proc = self.__compileUI(fn, True, progress)
969 if proc is not None:
970 while proc.state() == QProcess.ProcessState.Running:
971 QApplication.processEvents()
972 QThread.msleep(300)
973 QApplication.processEvents()
974 else:
975 break
976 progress.setValue(numForms)
977
978 def compileChangedForms(self):
979 """
980 Public method to compile all changed forms to source files.
981 """
982 if self.hooks["compileChangedForms"] is not None:
983 self.hooks["compileChangedForms"](self.project.pdata["FORMS"])
984 else:
985 if self.project.getProjectType() not in [
986 "PyQt5", "PyQt6", "E6Plugin", "PySide2", "PySide6"
987 ]:
988 # ignore the request for non Qt GUI projects
989 return
990
991 progress = E5ProgressDialog(
992 self.tr("Determining changed forms..."),
993 self.tr("Abort"), 0, 100, self.tr("%v/%m Forms"))
994 progress.setMinimumDuration(0)
995 progress.setWindowTitle(self.tr("Forms"))
996
997 # get list of changed forms
998 changedForms = []
999 progress.setMaximum(len(self.project.pdata["FORMS"]))
1000 for prog, fn in enumerate(self.project.pdata["FORMS"]):
1001 progress.setValue(prog)
1002 QApplication.processEvents()
1003
1004 ifn = os.path.join(self.project.ppath, fn)
1005 if self.project.getProjectLanguage() == "Python3":
1006 dirname, filename = os.path.split(os.path.splitext(ifn)[0])
1007 ofn = os.path.join(dirname, "Ui_" + filename + ".py")
1008 elif self.project.getProjectLanguage() == "Ruby":
1009 ofn = os.path.splitext(ifn)[0] + '.rb'
1010 if (
1011 not os.path.exists(ofn) or
1012 os.stat(ifn).st_mtime > os.stat(ofn).st_mtime
1013 ):
1014 changedForms.append(fn)
1015 progress.setValue(len(self.project.pdata["FORMS"]))
1016 QApplication.processEvents()
1017
1018 if changedForms:
1019 progress.setLabelText(
1020 self.tr("Compiling changed forms..."))
1021 progress.setMaximum(len(changedForms))
1022 progress.setValue(prog)
1023 QApplication.processEvents()
1024 for prog, fn in enumerate(changedForms):
1025 progress.setValue(prog)
1026 if progress.wasCanceled():
1027 break
1028
1029 proc = self.__compileUI(fn, True, progress)
1030 if proc is not None:
1031 while proc.state() == QProcess.ProcessState.Running:
1032 QApplication.processEvents()
1033 QThread.msleep(300)
1034 QApplication.processEvents()
1035 else:
1036 break
1037 progress.setValue(len(changedForms))
1038 QApplication.processEvents()
1039
1040 def handlePreferencesChanged(self):
1041 """
1042 Public slot used to handle the preferencesChanged signal.
1043 """
1044 ProjectBaseBrowser.handlePreferencesChanged(self)
1045
1046 self.__resetUiCompiler()
1047
1048 def __configureUicCompiler(self):
1049 """
1050 Private slot to configure some non-common uic compiler options.
1051 """
1052 from .UicCompilerOptionsDialog import UicCompilerOptionsDialog
1053
1054 params = self.project.pdata["UICPARAMS"]
1055
1056 if self.project.getProjectType() in ["PyQt5", "PyQt6", "E6Plugin"]:
1057 dlg = UicCompilerOptionsDialog(params, self.getUiCompiler())
1058 if dlg.exec() == QDialog.DialogCode.Accepted:
1059 package, suffix, root = dlg.getData()
1060 if package != params["Package"]:
1061 params["Package"] = package
1062 self.project.setDirty(True)
1063 if suffix != params["RcSuffix"]:
1064 params["RcSuffix"] = suffix
1065 self.project.setDirty(True)
1066 if root != params["PackagesRoot"]:
1067 params["PackagesRoot"] = root
1068 self.project.setDirty(True)
1069
1070 ###########################################################################
1071 ## Support for hooks below
1072 ###########################################################################
1073
1074 def _initHookMethods(self):
1075 """
1076 Protected method to initialize the hooks dictionary.
1077
1078 Supported hook methods are:
1079 <ul>
1080 <li>compileForm: takes filename as parameter</li>
1081 <li>compileAllForms: takes list of filenames as parameter</li>
1082 <li>compileSelectedForms: takes list of filenames as parameter</li>
1083 <li>compileChangedForms: takes list of filenames as parameter</li>
1084 <li>generateDialogCode: takes filename as parameter</li>
1085 <li>newForm: takes full directory path of new file as parameter</li>
1086 <li>open: takes a filename as parameter</li>
1087 </ul>
1088
1089 <b>Note</b>: Filenames are relative to the project directory, if not
1090 specified differently.
1091 """
1092 self.hooks = {
1093 "compileForm": None,
1094 "compileAllForms": None,
1095 "compileChangedForms": None,
1096 "compileSelectedForms": None,
1097 "generateDialogCode": None,
1098 "newForm": None,
1099 "open": None,
1100 }

eric ide

mercurial