1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a class used to display the Sources part of the project.
8"""
9
10import os
11import contextlib
12
13from PyQt5.QtCore import pyqtSignal
14from PyQt5.QtWidgets import QDialog, QInputDialog, QMenu
15
16from E5Gui import E5MessageBox
17
18from UI.BrowserModel import (
19    BrowserFileItem, BrowserClassItem, BrowserMethodItem,
20    BrowserClassAttributeItem, BrowserImportItem
21)
22
23from .ProjectBrowserModel import (
24    ProjectBrowserFileItem, ProjectBrowserSimpleDirectoryItem,
25    ProjectBrowserDirectoryItem, ProjectBrowserSourceType
26)
27from .ProjectBaseBrowser import ProjectBaseBrowser
28
29import Utilities
30import UI.PixmapCache
31
32
33class ProjectSourcesBrowser(ProjectBaseBrowser):
34    """
35    A class used to display the Sources part of the project.
36
37    @signal showMenu(str, QMenu) emitted when a menu is about to be shown.
38        The name of the menu and a reference to the menu are given.
39    """
40    showMenu = pyqtSignal(str, QMenu)
41
42    def __init__(self, project, parent=None):
43        """
44        Constructor
45
46        @param project reference to the project object
47        @param parent parent widget of this browser (QWidget)
48        """
49        ProjectBaseBrowser.__init__(self, project, ProjectBrowserSourceType,
50                                    parent)
51
52        self.selectedItemsFilter = [ProjectBrowserFileItem,
53                                    ProjectBrowserSimpleDirectoryItem]
54
55        self.setWindowTitle(self.tr('Sources'))
56
57        self.setWhatsThis(self.tr(
58            """<b>Project Sources Browser</b>"""
59            """<p>This allows to easily see all sources contained in the"""
60            """ current project. Several actions can be executed via the"""
61            """ context menu.</p>"""
62        ))
63
64        project.prepareRepopulateItem.connect(self._prepareRepopulateItem)
65        project.completeRepopulateItem.connect(self._completeRepopulateItem)
66
67        self.codemetrics = None
68        self.codecoverage = None
69        self.profiledata = None
70        self.classDiagram = None
71        self.importsDiagram = None
72        self.packageDiagram = None
73        self.applicationDiagram = None
74        self.loadedDiagram = None
75
76    def __closeAllWindows(self):
77        """
78        Private method to close all project related windows.
79        """
80        self.codemetrics and self.codemetrics.close()
81        self.codecoverage and self.codecoverage.close()
82        self.profiledata and self.profiledata.close()
83        self.classDiagram and self.classDiagram.close()
84        self.importsDiagram and self.importsDiagram.close()
85        self.packageDiagram and self.packageDiagram.close()
86        self.applicationDiagram and self.applicationDiagram.close()
87        self.loadedDiagram and self.loadedDiagram.close()
88
89    def _projectClosed(self):
90        """
91        Protected slot to handle the projectClosed signal.
92        """
93        self.__closeAllWindows()
94        ProjectBaseBrowser._projectClosed(self)
95
96    def _createPopupMenus(self):
97        """
98        Protected overloaded method to generate the popup menu.
99        """
100        ProjectBaseBrowser._createPopupMenus(self)
101        self.sourceMenuActions = {}
102
103        if self.project.isPythonProject():
104            self.__createPythonPopupMenus()
105        elif self.project.isRubyProject():
106            self.__createRubyPopupMenus()
107        elif self.project.isJavaScriptProject():
108            self.__createJavaScriptPopupMenus()
109        else:
110            # assign generic source menu
111            self.mainMenu = self.sourceMenu
112
113    def __createPythonPopupMenus(self):
114        """
115        Private method to generate the popup menus for a Python project.
116        """
117        self.checksMenu = QMenu(self.tr('Check'))
118        self.checksMenu.aboutToShow.connect(self.__showContextMenuCheck)
119
120        self.menuShow = QMenu(self.tr('Show'))
121        self.menuShow.addAction(
122            self.tr('Code metrics...'), self.__showCodeMetrics)
123        self.coverageMenuAction = self.menuShow.addAction(
124            self.tr('Code coverage...'), self.__showCodeCoverage)
125        self.profileMenuAction = self.menuShow.addAction(
126            self.tr('Profile data...'), self.__showProfileData)
127        self.menuShow.aboutToShow.connect(self.__showContextMenuShow)
128
129        self.graphicsMenu = QMenu(self.tr('Diagrams'))
130        self.classDiagramAction = self.graphicsMenu.addAction(
131            self.tr("Class Diagram..."), self.__showClassDiagram)
132        self.graphicsMenu.addAction(
133            self.tr("Package Diagram..."), self.__showPackageDiagram)
134        self.importsDiagramAction = self.graphicsMenu.addAction(
135            self.tr("Imports Diagram..."), self.__showImportsDiagram)
136        self.graphicsMenu.addAction(
137            self.tr("Application Diagram..."),
138            self.__showApplicationDiagram)
139        self.graphicsMenu.addSeparator()
140        self.graphicsMenu.addAction(
141            UI.PixmapCache.getIcon("open"),
142            self.tr("Load Diagram..."), self.__loadDiagram)
143        self.graphicsMenu.aboutToShow.connect(self.__showContextMenuGraphics)
144
145        self.unittestAction = self.sourceMenu.addAction(
146            self.tr('Run unittest...'), self.handleUnittest)
147        self.sourceMenu.addSeparator()
148        act = self.sourceMenu.addAction(
149            self.tr('Rename file'), self._renameFile)
150        self.menuActions.append(act)
151        act = self.sourceMenu.addAction(
152            self.tr('Remove from project'), self._removeFile)
153        self.menuActions.append(act)
154        act = self.sourceMenu.addAction(
155            self.tr('Delete'), self.__deleteFile)
156        self.menuActions.append(act)
157        self.sourceMenu.addSeparator()
158        self.sourceMenu.addAction(
159            self.tr('New package...'), self.__addNewPackage)
160        self.sourceMenu.addAction(
161            self.tr('Add source files...'), self.__addSourceFiles)
162        self.sourceMenu.addAction(
163            self.tr('Add source directory...'), self.__addSourceDirectory)
164        self.sourceMenu.addSeparator()
165        act = self.sourceMenu.addMenu(self.graphicsMenu)
166        self.sourceMenu.addSeparator()
167        self.sourceMenu.addMenu(self.checksMenu)
168        self.sourceMenu.addSeparator()
169        self.sourceMenuActions["Show"] = self.sourceMenu.addMenu(self.menuShow)
170        self.sourceMenu.addSeparator()
171        self.sourceMenu.addAction(
172            self.tr('Copy Path to Clipboard'), self._copyToClipboard)
173        self.sourceMenu.addSeparator()
174        self.sourceMenu.addAction(
175            self.tr('Expand all directories'), self._expandAllDirs)
176        self.sourceMenu.addAction(
177            self.tr('Collapse all directories'), self._collapseAllDirs)
178        self.sourceMenu.addSeparator()
179        self.sourceMenu.addAction(self.tr('Configure...'), self._configure)
180
181        self.menu.addSeparator()
182        self.menu.addAction(
183            self.tr('New package...'), self.__addNewPackage)
184        self.menu.addAction(
185            self.tr('Add source files...'), self.__addSourceFiles)
186        self.menu.addAction(
187            self.tr('Add source directory...'), self.__addSourceDirectory)
188        self.menu.addSeparator()
189        self.menu.addAction(
190            self.tr('Expand all directories'), self._expandAllDirs)
191        self.menu.addAction(
192            self.tr('Collapse all directories'), self._collapseAllDirs)
193        self.menu.addSeparator()
194        self.menu.addAction(self.tr('Configure...'), self._configure)
195
196        # create the attribute menu
197        self.gotoMenu = QMenu(self.tr("Goto"), self)
198        self.gotoMenu.aboutToShow.connect(self._showGotoMenu)
199        self.gotoMenu.triggered.connect(self._gotoAttribute)
200
201        self.attributeMenu = QMenu(self)
202        self.attributeMenu.addMenu(self.gotoMenu)
203        self.attributeMenu.addSeparator()
204        self.attributeMenu.addAction(
205            self.tr('New package...'), self.__addNewPackage)
206        self.attributeMenu.addAction(
207            self.tr('Add source files...'), self.project.addSourceFiles)
208        self.attributeMenu.addAction(
209            self.tr('Add source directory...'), self.project.addSourceDir)
210        self.attributeMenu.addSeparator()
211        self.attributeMenu.addAction(
212            self.tr('Expand all directories'), self._expandAllDirs)
213        self.attributeMenu.addAction(
214            self.tr('Collapse all directories'), self._collapseAllDirs)
215        self.attributeMenu.addSeparator()
216        self.attributeMenu.addAction(
217            self.tr('Configure...'), self._configure)
218
219        self.backMenu = QMenu(self)
220        self.backMenu.addAction(
221            self.tr('New package...'), self.__addNewPackage)
222        self.backMenu.addAction(
223            self.tr('Add source files...'), self.project.addSourceFiles)
224        self.backMenu.addAction(
225            self.tr('Add source directory...'), self.project.addSourceDir)
226        self.backMenu.addSeparator()
227        self.backMenu.addAction(
228            self.tr('Expand all directories'), self._expandAllDirs)
229        self.backMenu.addAction(
230            self.tr('Collapse all directories'), self._collapseAllDirs)
231        self.backMenu.addSeparator()
232        self.backMenu.addAction(self.tr('Configure...'), self._configure)
233        self.backMenu.setEnabled(False)
234
235        self.multiMenu.addSeparator()
236        act = self.multiMenu.addAction(
237            self.tr('Remove from project'), self._removeFile)
238        self.multiMenuActions.append(act)
239        act = self.multiMenu.addAction(
240            self.tr('Delete'), self.__deleteFile)
241        self.multiMenuActions.append(act)
242        self.multiMenu.addSeparator()
243        self.multiMenu.addMenu(self.checksMenu)
244        self.multiMenu.addSeparator()
245        self.multiMenu.addAction(
246            self.tr('Expand all directories'), self._expandAllDirs)
247        self.multiMenu.addAction(
248            self.tr('Collapse all directories'), self._collapseAllDirs)
249        self.multiMenu.addSeparator()
250        self.multiMenu.addAction(self.tr('Configure...'), self._configure)
251
252        self.dirMenu = QMenu(self)
253        act = self.dirMenu.addAction(
254            self.tr('Remove from project'), self._removeDir)
255        self.dirMenuActions.append(act)
256        act = self.dirMenu.addAction(
257            self.tr('Delete'), self._deleteDirectory)
258        self.dirMenuActions.append(act)
259        self.dirMenu.addSeparator()
260        self.dirMenu.addAction(
261            self.tr('New package...'), self.__addNewPackage)
262        self.dirMenu.addAction(
263            self.tr('Add source files...'), self.__addSourceFiles)
264        self.dirMenu.addAction(
265            self.tr('Add source directory...'), self.__addSourceDirectory)
266        self.dirMenu.addSeparator()
267        act = self.dirMenu.addMenu(self.graphicsMenu)
268        self.dirMenu.addSeparator()
269        self.dirMenu.addMenu(self.checksMenu)
270        self.dirMenu.addSeparator()
271        self.dirMenu.addAction(
272            self.tr('Copy Path to Clipboard'), self._copyToClipboard)
273        self.dirMenu.addSeparator()
274        self.dirMenu.addAction(
275            self.tr('Expand all directories'), self._expandAllDirs)
276        self.dirMenu.addAction(
277            self.tr('Collapse all directories'), self._collapseAllDirs)
278        self.dirMenu.addSeparator()
279        self.dirMenu.addAction(self.tr('Configure...'), self._configure)
280
281        self.dirMultiMenu = QMenu(self)
282        self.dirMultiMenu.addAction(
283            self.tr('Expand all directories'), self._expandAllDirs)
284        self.dirMultiMenu.addAction(
285            self.tr('Collapse all directories'), self._collapseAllDirs)
286        self.dirMultiMenu.addSeparator()
287        self.dirMultiMenu.addAction(
288            self.tr('Configure...'), self._configure)
289
290        self.sourceMenu.aboutToShow.connect(self.__showContextMenu)
291        self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
292        self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
293        self.dirMultiMenu.aboutToShow.connect(self.__showContextMenuDirMulti)
294        self.backMenu.aboutToShow.connect(self.__showContextMenuBack)
295        self.mainMenu = self.sourceMenu
296
297    def __createRubyPopupMenus(self):
298        """
299        Private method to generate the popup menus for a Ruby project.
300        """
301        self.graphicsMenu = QMenu(self.tr('Diagrams'))
302        self.classDiagramAction = self.graphicsMenu.addAction(
303            self.tr("Class Diagram..."), self.__showClassDiagram)
304        self.graphicsMenu.addAction(
305            self.tr("Package Diagram..."), self.__showPackageDiagram)
306        self.graphicsMenu.addAction(
307            self.tr("Application Diagram..."),
308            self.__showApplicationDiagram)
309        self.graphicsMenu.addSeparator()
310        self.graphicsMenu.addAction(
311            UI.PixmapCache.getIcon("fileOpen"),
312            self.tr("Load Diagram..."), self.__loadDiagram)
313
314        self.sourceMenu.addSeparator()
315        act = self.sourceMenu.addAction(
316            self.tr('Rename file'), self._renameFile)
317        self.menuActions.append(act)
318        act = self.sourceMenu.addAction(
319            self.tr('Remove from project'), self._removeFile)
320        self.menuActions.append(act)
321        act = self.sourceMenu.addAction(
322            self.tr('Delete'), self.__deleteFile)
323        self.menuActions.append(act)
324        self.sourceMenu.addSeparator()
325        self.sourceMenu.addAction(
326            self.tr('Add source files...'), self.__addSourceFiles)
327        self.sourceMenu.addAction(
328            self.tr('Add source directory...'), self.__addSourceDirectory)
329        self.sourceMenu.addSeparator()
330        act = self.sourceMenu.addMenu(self.graphicsMenu)
331        self.sourceMenu.addSeparator()
332        self.sourceMenu.addAction(
333            self.tr('Expand all directories'), self._expandAllDirs)
334        self.sourceMenu.addAction(
335            self.tr('Collapse all directories'), self._collapseAllDirs)
336        self.sourceMenu.addSeparator()
337        self.sourceMenu.addAction(self.tr('Configure...'), self._configure)
338
339        self.menu.addSeparator()
340        self.menu.addAction(
341            self.tr('Add source files...'), self.__addSourceFiles)
342        self.menu.addAction(
343            self.tr('Add source directory...'), self.__addSourceDirectory)
344        self.menu.addSeparator()
345        self.menu.addAction(
346            self.tr('Expand all directories'), self._expandAllDirs)
347        self.menu.addAction(
348            self.tr('Collapse all directories'), self._collapseAllDirs)
349        self.menu.addSeparator()
350        self.menu.addAction(self.tr('Configure...'), self._configure)
351
352        # create the attribute menu
353        self.gotoMenu = QMenu(self.tr("Goto"), self)
354        self.gotoMenu.aboutToShow.connect(self._showGotoMenu)
355        self.gotoMenu.triggered.connect(self._gotoAttribute)
356
357        self.attributeMenu = QMenu(self)
358        self.attributeMenu.addMenu(self.gotoMenu)
359        self.attributeMenu.addSeparator()
360        self.attributeMenu.addAction(
361            self.tr('Add source files...'), self.project.addSourceFiles)
362        self.attributeMenu.addAction(
363            self.tr('Add source directory...'), self.project.addSourceDir)
364        self.attributeMenu.addSeparator()
365        self.attributeMenu.addAction(
366            self.tr('Expand all directories'), self._expandAllDirs)
367        self.attributeMenu.addAction(
368            self.tr('Collapse all directories'), self._collapseAllDirs)
369        self.attributeMenu.addSeparator()
370        self.attributeMenu.addAction(
371            self.tr('Configure...'), self._configure)
372
373        self.backMenu = QMenu(self)
374        self.backMenu.addAction(
375            self.tr('Add source files...'), self.project.addSourceFiles)
376        self.backMenu.addAction(
377            self.tr('Add source directory...'), self.project.addSourceDir)
378        self.backMenu.addSeparator()
379        self.backMenu.addAction(
380            self.tr('Expand all directories'), self._expandAllDirs)
381        self.backMenu.addAction(
382            self.tr('Collapse all directories'), self._collapseAllDirs)
383        self.backMenu.setEnabled(False)
384        self.backMenu.addSeparator()
385        self.backMenu.addAction(self.tr('Configure...'), self._configure)
386
387        self.multiMenu.addSeparator()
388        act = self.multiMenu.addAction(
389            self.tr('Remove from project'), self._removeFile)
390        self.multiMenuActions.append(act)
391        act = self.multiMenu.addAction(
392            self.tr('Delete'), self.__deleteFile)
393        self.multiMenuActions.append(act)
394        self.multiMenu.addSeparator()
395        self.multiMenu.addAction(
396            self.tr('Expand all directories'), self._expandAllDirs)
397        self.multiMenu.addAction(
398            self.tr('Collapse all directories'), self._collapseAllDirs)
399        self.multiMenu.addSeparator()
400        self.multiMenu.addAction(self.tr('Configure...'), self._configure)
401
402        self.dirMenu = QMenu(self)
403        act = self.dirMenu.addAction(
404            self.tr('Remove from project'), self._removeDir)
405        self.dirMenuActions.append(act)
406        self.dirMenu.addSeparator()
407        self.dirMenu.addAction(
408            self.tr('Add source files...'), self.__addSourceFiles)
409        self.dirMenu.addAction(
410            self.tr('Add source directory...'), self.__addSourceDirectory)
411        self.dirMenu.addSeparator()
412        act = self.dirMenu.addMenu(self.graphicsMenu)
413        self.dirMenu.addSeparator()
414        self.dirMenu.addAction(
415            self.tr('Expand all directories'), self._expandAllDirs)
416        self.dirMenu.addAction(
417            self.tr('Collapse all directories'), self._collapseAllDirs)
418        self.dirMenu.addSeparator()
419        self.dirMenu.addAction(self.tr('Configure...'), self._configure)
420
421        self.dirMultiMenu = QMenu(self)
422        self.dirMultiMenu.addAction(
423            self.tr('Expand all directories'), self._expandAllDirs)
424        self.dirMultiMenu.addAction(
425            self.tr('Collapse all directories'), self._collapseAllDirs)
426        self.dirMultiMenu.addSeparator()
427        self.dirMultiMenu.addAction(
428            self.tr('Configure...'), self._configure)
429
430        self.sourceMenu.aboutToShow.connect(self.__showContextMenu)
431        self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
432        self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
433        self.dirMultiMenu.aboutToShow.connect(self.__showContextMenuDirMulti)
434        self.backMenu.aboutToShow.connect(self.__showContextMenuBack)
435        self.mainMenu = self.sourceMenu
436
437    def __createJavaScriptPopupMenus(self):
438        """
439        Private method to generate the popup menus for a Python project.
440        """
441        self.checksMenu = QMenu(self.tr('Check'))
442        self.checksMenu.aboutToShow.connect(self.__showContextMenuCheck)
443
444        self.sourceMenu.addSeparator()
445        act = self.sourceMenu.addAction(
446            self.tr('Rename file'), self._renameFile)
447        self.menuActions.append(act)
448        act = self.sourceMenu.addAction(
449            self.tr('Remove from project'), self._removeFile)
450        self.menuActions.append(act)
451        act = self.sourceMenu.addAction(
452            self.tr('Delete'), self.__deleteFile)
453        self.menuActions.append(act)
454        self.sourceMenu.addSeparator()
455        self.sourceMenu.addAction(
456            self.tr('Add source files...'), self.__addSourceFiles)
457        self.sourceMenu.addAction(
458            self.tr('Add source directory...'), self.__addSourceDirectory)
459        self.sourceMenu.addSeparator()
460        self.sourceMenu.addMenu(self.checksMenu)
461        self.sourceMenu.addSeparator()
462        self.sourceMenu.addAction(
463            self.tr('Copy Path to Clipboard'), self._copyToClipboard)
464        self.sourceMenu.addSeparator()
465        self.sourceMenu.addAction(
466            self.tr('Expand all directories'), self._expandAllDirs)
467        self.sourceMenu.addAction(
468            self.tr('Collapse all directories'), self._collapseAllDirs)
469        self.sourceMenu.addSeparator()
470        self.sourceMenu.addAction(self.tr('Configure...'), self._configure)
471
472        self.menu.addSeparator()
473        self.menu.addAction(
474            self.tr('Add source files...'), self.__addSourceFiles)
475        self.menu.addAction(
476            self.tr('Add source directory...'), self.__addSourceDirectory)
477        self.menu.addSeparator()
478        self.menu.addAction(
479            self.tr('Expand all directories'), self._expandAllDirs)
480        self.menu.addAction(
481            self.tr('Collapse all directories'), self._collapseAllDirs)
482        self.menu.addSeparator()
483        self.menu.addAction(self.tr('Configure...'), self._configure)
484
485        # create the attribute menu
486        self.gotoMenu = QMenu(self.tr("Goto"), self)
487        self.gotoMenu.aboutToShow.connect(self._showGotoMenu)
488        self.gotoMenu.triggered.connect(self._gotoAttribute)
489
490        self.attributeMenu = QMenu(self)
491        self.attributeMenu.addMenu(self.gotoMenu)
492        self.attributeMenu.addSeparator()
493        self.attributeMenu.addAction(
494            self.tr('Add source files...'), self.project.addSourceFiles)
495        self.attributeMenu.addAction(
496            self.tr('Add source directory...'), self.project.addSourceDir)
497        self.attributeMenu.addSeparator()
498        self.attributeMenu.addAction(
499            self.tr('Expand all directories'), self._expandAllDirs)
500        self.attributeMenu.addAction(
501            self.tr('Collapse all directories'), self._collapseAllDirs)
502        self.attributeMenu.addSeparator()
503        self.attributeMenu.addAction(
504            self.tr('Configure...'), self._configure)
505
506        self.backMenu = QMenu(self)
507        self.backMenu.addAction(
508            self.tr('Add source files...'), self.project.addSourceFiles)
509        self.backMenu.addAction(
510            self.tr('Add source directory...'), self.project.addSourceDir)
511        self.backMenu.addSeparator()
512        self.backMenu.addAction(
513            self.tr('Expand all directories'), self._expandAllDirs)
514        self.backMenu.addAction(
515            self.tr('Collapse all directories'), self._collapseAllDirs)
516        self.backMenu.addSeparator()
517        self.backMenu.addAction(self.tr('Configure...'), self._configure)
518        self.backMenu.setEnabled(False)
519
520        self.multiMenu.addSeparator()
521        act = self.multiMenu.addAction(
522            self.tr('Remove from project'), self._removeFile)
523        self.multiMenuActions.append(act)
524        act = self.multiMenu.addAction(
525            self.tr('Delete'), self.__deleteFile)
526        self.multiMenuActions.append(act)
527        self.multiMenu.addSeparator()
528        self.multiMenu.addMenu(self.checksMenu)
529        self.multiMenu.addSeparator()
530        self.multiMenu.addAction(
531            self.tr('Expand all directories'), self._expandAllDirs)
532        self.multiMenu.addAction(
533            self.tr('Collapse all directories'), self._collapseAllDirs)
534        self.multiMenu.addSeparator()
535        self.multiMenu.addAction(self.tr('Configure...'), self._configure)
536
537        self.dirMenu = QMenu(self)
538        act = self.dirMenu.addAction(
539            self.tr('Remove from project'), self._removeDir)
540        self.dirMenuActions.append(act)
541        act = self.dirMenu.addAction(
542            self.tr('Delete'), self._deleteDirectory)
543        self.dirMenuActions.append(act)
544        self.dirMenu.addSeparator()
545        self.dirMenu.addAction(
546            self.tr('Add source files...'), self.__addSourceFiles)
547        self.dirMenu.addAction(
548            self.tr('Add source directory...'), self.__addSourceDirectory)
549        self.dirMenu.addSeparator()
550        self.dirMenu.addMenu(self.checksMenu)
551        self.dirMenu.addSeparator()
552        self.dirMenu.addAction(
553            self.tr('Copy Path to Clipboard'), self._copyToClipboard)
554        self.dirMenu.addSeparator()
555        self.dirMenu.addAction(
556            self.tr('Expand all directories'), self._expandAllDirs)
557        self.dirMenu.addAction(
558            self.tr('Collapse all directories'), self._collapseAllDirs)
559        self.dirMenu.addSeparator()
560        self.dirMenu.addAction(self.tr('Configure...'), self._configure)
561
562        self.dirMultiMenu = QMenu(self)
563        self.dirMultiMenu.addAction(
564            self.tr('Expand all directories'), self._expandAllDirs)
565        self.dirMultiMenu.addAction(
566            self.tr('Collapse all directories'), self._collapseAllDirs)
567        self.dirMultiMenu.addSeparator()
568        self.dirMultiMenu.addAction(
569            self.tr('Configure...'), self._configure)
570
571        self.sourceMenu.aboutToShow.connect(self.__showContextMenu)
572        self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
573        self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
574        self.dirMultiMenu.aboutToShow.connect(self.__showContextMenuDirMulti)
575        self.backMenu.aboutToShow.connect(self.__showContextMenuBack)
576        self.mainMenu = self.sourceMenu
577
578    def _contextMenuRequested(self, coord):
579        """
580        Protected slot to show the context menu.
581
582        @param coord the position of the mouse pointer (QPoint)
583        """
584        if not self.project.isOpen():
585            return
586
587        with contextlib.suppress(Exception):
588            categories = self.getSelectedItemsCountCategorized(
589                [ProjectBrowserFileItem, BrowserClassItem,
590                 BrowserMethodItem, ProjectBrowserSimpleDirectoryItem,
591                 BrowserClassAttributeItem, BrowserImportItem])
592            cnt = categories["sum"]
593            if cnt <= 1:
594                index = self.indexAt(coord)
595                if index.isValid():
596                    self._selectSingleItem(index)
597                    categories = self.getSelectedItemsCountCategorized(
598                        [ProjectBrowserFileItem, BrowserClassItem,
599                         BrowserMethodItem, ProjectBrowserSimpleDirectoryItem,
600                         BrowserClassAttributeItem, BrowserImportItem])
601                    cnt = categories["sum"]
602
603            bfcnt = categories[str(ProjectBrowserFileItem)]
604            cmcnt = (
605                categories[str(BrowserClassItem)] +
606                categories[str(BrowserMethodItem)] +
607                categories[str(BrowserClassAttributeItem)] +
608                categories[str(BrowserImportItem)]
609            )
610            sdcnt = categories[str(ProjectBrowserSimpleDirectoryItem)]
611            if cnt > 1 and cnt == bfcnt:
612                self.multiMenu.popup(self.mapToGlobal(coord))
613            elif cnt > 1 and cnt == sdcnt:
614                self.dirMultiMenu.popup(self.mapToGlobal(coord))
615            else:
616                index = self.indexAt(coord)
617                if cnt == 1 and index.isValid():
618                    if bfcnt == 1 or cmcnt == 1:
619                        itm = self.model().item(index)
620                        if isinstance(itm, ProjectBrowserFileItem):
621                            fn = itm.fileName()
622                            if self.project.isPythonProject():
623                                if fn.endswith('.ptl'):
624                                    for act in self.sourceMenuActions.values():
625                                        act.setEnabled(False)
626                                    self.classDiagramAction.setEnabled(True)
627                                    self.importsDiagramAction.setEnabled(True)
628                                    self.unittestAction.setEnabled(False)
629                                    self.checksMenu.menuAction().setEnabled(
630                                        False)
631                                elif fn.endswith('.rb'):
632                                    # entry for mixed mode programs
633                                    for act in self.sourceMenuActions.values():
634                                        act.setEnabled(False)
635                                    self.classDiagramAction.setEnabled(True)
636                                    self.importsDiagramAction.setEnabled(False)
637                                    self.unittestAction.setEnabled(False)
638                                    self.checksMenu.menuAction().setEnabled(
639                                        False)
640                                elif fn.endswith('.js'):
641                                    # entry for mixed mode programs
642                                    for act in self.sourceMenuActions.values():
643                                        act.setEnabled(False)
644                                    self.unittestAction.setEnabled(False)
645                                    self.checksMenu.menuAction().setEnabled(
646                                        False)
647                                    self.graphicsMenu.menuAction().setEnabled(
648                                        False)
649                                else:
650                                    # assume the source file is a Python file
651                                    for act in self.sourceMenuActions.values():
652                                        act.setEnabled(True)
653                                    self.classDiagramAction.setEnabled(True)
654                                    self.importsDiagramAction.setEnabled(True)
655                                    self.unittestAction.setEnabled(True)
656                                    self.checksMenu.menuAction().setEnabled(
657                                        True)
658                            self.sourceMenu.popup(self.mapToGlobal(coord))
659                        elif isinstance(
660                            itm,
661                            (BrowserClassItem, BrowserMethodItem,
662                             BrowserImportItem)
663                        ):
664                            self.menu.popup(self.mapToGlobal(coord))
665                        elif isinstance(itm, BrowserClassAttributeItem):
666                            self.attributeMenu.popup(self.mapToGlobal(coord))
667                        else:
668                            self.backMenu.popup(self.mapToGlobal(coord))
669                    elif sdcnt == 1:
670                        self.classDiagramAction.setEnabled(False)
671                        self.dirMenu.popup(self.mapToGlobal(coord))
672                    else:
673                        self.backMenu.popup(self.mapToGlobal(coord))
674                else:
675                    self.backMenu.popup(self.mapToGlobal(coord))
676
677    def __showContextMenu(self):
678        """
679        Private slot called by the sourceMenu aboutToShow signal.
680        """
681        ProjectBaseBrowser._showContextMenu(self, self.sourceMenu)
682
683        self.showMenu.emit("Main", self.sourceMenu)
684
685    def __showContextMenuMulti(self):
686        """
687        Private slot called by the multiMenu aboutToShow signal.
688        """
689        ProjectBaseBrowser._showContextMenuMulti(self, self.multiMenu)
690
691        self.showMenu.emit("MainMulti", self.multiMenu)
692
693    def __showContextMenuDir(self):
694        """
695        Private slot called by the dirMenu aboutToShow signal.
696        """
697        ProjectBaseBrowser._showContextMenuDir(self, self.dirMenu)
698
699        self.showMenu.emit("MainDir", self.dirMenu)
700
701    def __showContextMenuDirMulti(self):
702        """
703        Private slot called by the dirMultiMenu aboutToShow signal.
704        """
705        ProjectBaseBrowser._showContextMenuDirMulti(self, self.dirMultiMenu)
706
707        self.showMenu.emit("MainDirMulti", self.dirMultiMenu)
708
709    def __showContextMenuBack(self):
710        """
711        Private slot called by the backMenu aboutToShow signal.
712        """
713        ProjectBaseBrowser._showContextMenuBack(self, self.backMenu)
714
715        self.showMenu.emit("MainBack", self.backMenu)
716
717    def __showContextMenuShow(self):
718        """
719        Private slot called before the show menu is shown.
720        """
721        prEnable = False
722        coEnable = False
723
724        # first check if the file belongs to a project and there is
725        # a project coverage file
726        fn = self.project.getMainScript(True)
727        if fn is not None:
728            tfn = Utilities.getTestFileName(fn)
729            basename = os.path.splitext(fn)[0]
730            tbasename = os.path.splitext(tfn)[0]
731            prEnable = (
732                prEnable or
733                os.path.isfile("{0}.profile".format(basename)) or
734                os.path.isfile("{0}.profile".format(tbasename))
735            )
736            coEnable = (
737                (coEnable or
738                 os.path.isfile("{0}.coverage".format(basename)) or
739                 os.path.isfile("{0}.coverage".format(tbasename))) and
740                self.project.isPy3Project()
741            )
742
743        # now check the selected item
744        itm = self.model().item(self.currentIndex())
745        fn = itm.fileName()
746        if fn is not None:
747            basename = os.path.splitext(fn)[0]
748            prEnable = (
749                prEnable or
750                os.path.isfile("{0}.profile".format(basename))
751            )
752            coEnable = (
753                (coEnable or
754                 os.path.isfile("{0}.coverage".format(basename))) and
755                itm.isPython3File()
756            )
757
758        self.profileMenuAction.setEnabled(prEnable)
759        self.coverageMenuAction.setEnabled(coEnable)
760
761        self.showMenu.emit("Show", self.menuShow)
762
763    def _openItem(self):
764        """
765        Protected slot to handle the open popup menu entry.
766        """
767        itmList = self.getSelectedItems(
768            [BrowserFileItem, BrowserClassItem, BrowserMethodItem,
769             BrowserClassAttributeItem, BrowserImportItem])
770
771        for itm in itmList:
772            if isinstance(itm, BrowserFileItem):
773                if itm.isPython3File():
774                    self.sourceFile[str].emit(itm.fileName())
775                elif itm.isRubyFile():
776                    self.sourceFile[str, int, str].emit(
777                        itm.fileName(), -1, "Ruby")
778                elif itm.isDFile():
779                    self.sourceFile[str, int, str].emit(
780                        itm.fileName(), -1, "D")
781                else:
782                    self.sourceFile[str].emit(itm.fileName())
783            elif isinstance(itm, BrowserClassItem):
784                self.sourceFile[str, int].emit(
785                    itm.fileName(), itm.classObject().lineno)
786            elif isinstance(itm, BrowserMethodItem):
787                self.sourceFile[str, int].emit(
788                    itm.fileName(), itm.functionObject().lineno)
789            elif isinstance(itm, BrowserClassAttributeItem):
790                self.sourceFile[str, int].emit(
791                    itm.fileName(), itm.attributeObject().lineno)
792            elif isinstance(itm, BrowserImportItem):
793                self.sourceFile[str, list].emit(
794                    itm.fileName(), itm.linenos())
795
796    def __addNewPackage(self):
797        """
798        Private method to add a new package to the project.
799        """
800        itm = self.model().item(self.currentIndex())
801        if isinstance(
802            itm,
803            (ProjectBrowserFileItem, BrowserClassItem, BrowserMethodItem)
804        ):
805            dn = os.path.dirname(itm.fileName())
806        elif isinstance(
807            itm,
808            (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem)
809        ):
810            dn = itm.dirName()
811        else:
812            dn = ""
813
814        dn = self.project.getRelativePath(dn)
815        if dn.startswith(os.sep):
816            dn = dn[1:]
817        from .NewPythonPackageDialog import NewPythonPackageDialog
818        dlg = NewPythonPackageDialog(dn, self)
819        if dlg.exec() == QDialog.DialogCode.Accepted:
820            packageName = dlg.getData()
821            nameParts = packageName.split(".")
822            packagePath = self.project.ppath
823            packageFile = ""
824            for name in nameParts:
825                packagePath = os.path.join(packagePath, name)
826                if not os.path.exists(packagePath):
827                    try:
828                        os.mkdir(packagePath)
829                    except OSError as err:
830                        E5MessageBox.critical(
831                            self,
832                            self.tr("Add new Python package"),
833                            self.tr(
834                                """<p>The package directory <b>{0}</b> could"""
835                                """ not be created. Aborting...</p>"""
836                                """<p>Reason: {1}</p>""")
837                            .format(packagePath, str(err)))
838                        return
839                packageFile = os.path.join(packagePath, "__init__.py")
840                if not os.path.exists(packageFile):
841                    try:
842                        with open(packageFile, "w", encoding="utf-8"):
843                            pass
844                    except OSError as err:
845                        E5MessageBox.critical(
846                            self,
847                            self.tr("Add new Python package"),
848                            self.tr(
849                                """<p>The package file <b>{0}</b> could"""
850                                """ not be created. Aborting...</p>"""
851                                """<p>Reason: {1}</p>""")
852                            .format(packageFile, str(err)))
853                        return
854                self.project.appendFile(packageFile)
855            if packageFile:
856                self.sourceFile[str].emit(packageFile)
857
858    def __addSourceFiles(self):
859        """
860        Private method to add a source file to the project.
861        """
862        itm = self.model().item(self.currentIndex())
863        if isinstance(
864            itm,
865            (ProjectBrowserFileItem, BrowserClassItem, BrowserMethodItem)
866        ):
867            dn = os.path.dirname(itm.fileName())
868        elif isinstance(
869            itm,
870            (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem)
871        ):
872            dn = itm.dirName()
873        else:
874            dn = None
875        self.project.addFiles('source', dn)
876
877    def __addSourceDirectory(self):
878        """
879        Private method to add source files of a directory to the project.
880        """
881        itm = self.model().item(self.currentIndex())
882        if isinstance(
883            itm,
884            (ProjectBrowserFileItem, BrowserClassItem, BrowserMethodItem)
885        ):
886            dn = os.path.dirname(itm.fileName())
887        elif isinstance(
888            itm,
889            (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem)
890        ):
891            dn = itm.dirName()
892        else:
893            dn = None
894        self.project.addDirectory('source', dn)
895
896    def __deleteFile(self):
897        """
898        Private method to delete files from the project.
899        """
900        itmList = self.getSelectedItems()
901
902        files = []
903        fullNames = []
904        for itm in itmList:
905            fn2 = itm.fileName()
906            fullNames.append(fn2)
907            fn = self.project.getRelativePath(fn2)
908            files.append(fn)
909
910        from UI.DeleteFilesConfirmationDialog import (
911            DeleteFilesConfirmationDialog
912        )
913        dlg = DeleteFilesConfirmationDialog(
914            self.parent(),
915            self.tr("Delete files"),
916            self.tr(
917                "Do you really want to delete these files from the project?"),
918            files)
919
920        if dlg.exec() == QDialog.DialogCode.Accepted:
921            for fn2, fn in zip(fullNames, files):
922                self.closeSourceWindow.emit(fn2)
923                self.project.deleteFile(fn)
924
925    ###########################################################################
926    ## Methods for the Checks submenu
927    ###########################################################################
928
929    def __showContextMenuCheck(self):
930        """
931        Private slot called before the checks menu is shown.
932        """
933        self.showMenu.emit("Checks", self.checksMenu)
934
935    ###########################################################################
936    ## Methods for the Show submenu
937    ###########################################################################
938
939    def __showCodeMetrics(self):
940        """
941        Private method to handle the code metrics context menu action.
942        """
943        itm = self.model().item(self.currentIndex())
944        fn = itm.fileName()
945
946        from DataViews.CodeMetricsDialog import CodeMetricsDialog
947        self.codemetrics = CodeMetricsDialog()
948        self.codemetrics.show()
949        self.codemetrics.start(fn)
950
951    def __showCodeCoverage(self):
952        """
953        Private method to handle the code coverage context menu action.
954        """
955        itm = self.model().item(self.currentIndex())
956        fn = itm.fileName()
957        pfn = self.project.getMainScript(True)
958
959        files = []
960
961        if pfn is not None:
962            tpfn = Utilities.getTestFileName(pfn)
963            basename = os.path.splitext(pfn)[0]
964            tbasename = os.path.splitext(tpfn)[0]
965
966            f = "{0}.coverage".format(basename)
967            tf = "{0}.coverage".format(tbasename)
968            if os.path.isfile(f):
969                files.append(f)
970            if os.path.isfile(tf):
971                files.append(tf)
972
973        if fn is not None:
974            tfn = Utilities.getTestFileName(fn)
975            basename = os.path.splitext(fn)[0]
976            tbasename = os.path.splitext(tfn)[0]
977
978            f = "{0}.coverage".format(basename)
979            tf = "{0}.coverage".format(tbasename)
980            if os.path.isfile(f) and f not in files:
981                files.append(f)
982            if os.path.isfile(tf) and tf not in files:
983                files.append(tf)
984
985        if files:
986            if len(files) > 1:
987                pfn, ok = QInputDialog.getItem(
988                    None,
989                    self.tr("Code Coverage"),
990                    self.tr("Please select a coverage file"),
991                    files,
992                    0, False)
993                if not ok:
994                    return
995            else:
996                pfn = files[0]
997        else:
998            return
999
1000        from DataViews.PyCoverageDialog import PyCoverageDialog
1001        self.codecoverage = PyCoverageDialog()
1002        self.codecoverage.show()
1003        self.codecoverage.start(pfn, fn)
1004
1005    def __showProfileData(self):
1006        """
1007        Private method to handle the show profile data context menu action.
1008        """
1009        itm = self.model().item(self.currentIndex())
1010        fn = itm.fileName()
1011        pfn = self.project.getMainScript(True)
1012
1013        files = []
1014
1015        if pfn is not None:
1016            tpfn = Utilities.getTestFileName(pfn)
1017            basename = os.path.splitext(pfn)[0]
1018            tbasename = os.path.splitext(tpfn)[0]
1019
1020            f = "{0}.profile".format(basename)
1021            tf = "{0}.profile".format(tbasename)
1022            if os.path.isfile(f):
1023                files.append(f)
1024            if os.path.isfile(tf):
1025                files.append(tf)
1026
1027        if fn is not None:
1028            tfn = Utilities.getTestFileName(fn)
1029            basename = os.path.splitext(fn)[0]
1030            tbasename = os.path.splitext(tfn)[0]
1031
1032            f = "{0}.profile".format(basename)
1033            tf = "{0}.profile".format(tbasename)
1034            if os.path.isfile(f) and f not in files:
1035                files.append(f)
1036            if os.path.isfile(tf) and tf not in files:
1037                files.append(tf)
1038
1039        if files:
1040            if len(files) > 1:
1041                pfn, ok = QInputDialog.getItem(
1042                    None,
1043                    self.tr("Profile Data"),
1044                    self.tr("Please select a profile file"),
1045                    files,
1046                    0, False)
1047                if not ok:
1048                    return
1049            else:
1050                pfn = files[0]
1051        else:
1052            return
1053
1054        from DataViews.PyProfileDialog import PyProfileDialog
1055        self.profiledata = PyProfileDialog()
1056        self.profiledata.show()
1057        self.profiledata.start(pfn, fn)
1058
1059    ###########################################################################
1060    ## Methods for the Graphics submenu
1061    ###########################################################################
1062
1063    def __showContextMenuGraphics(self):
1064        """
1065        Private slot called before the checks menu is shown.
1066        """
1067        self.showMenu.emit("Graphics", self.graphicsMenu)
1068
1069    def __showClassDiagram(self):
1070        """
1071        Private method to handle the class diagram context menu action.
1072        """
1073        itm = self.model().item(self.currentIndex())
1074        try:
1075            fn = itm.fileName()
1076        except AttributeError:
1077            fn = itm.dirName()
1078        res = E5MessageBox.yesNo(
1079            self,
1080            self.tr("Class Diagram"),
1081            self.tr("""Include class attributes?"""),
1082            yesDefault=True)
1083
1084        from Graphics.UMLDialog import UMLDialog, UMLDialogType
1085        self.classDiagram = UMLDialog(
1086            UMLDialogType.CLASS_DIAGRAM, self.project, fn,
1087            self, noAttrs=not res)
1088        self.classDiagram.show()
1089
1090    def __showImportsDiagram(self):
1091        """
1092        Private method to handle the imports diagram context menu action.
1093        """
1094        itm = self.model().item(self.currentIndex())
1095        try:
1096            fn = itm.fileName()
1097        except AttributeError:
1098            fn = itm.dirName()
1099        package = fn if os.path.isdir(fn) else os.path.dirname(fn)
1100        res = E5MessageBox.yesNo(
1101            self,
1102            self.tr("Imports Diagram"),
1103            self.tr("""Include imports from external modules?"""))
1104
1105        from Graphics.UMLDialog import UMLDialog, UMLDialogType
1106        self.importsDiagram = UMLDialog(
1107            UMLDialogType.IMPORTS_DIAGRAM, self.project, package,
1108            self, showExternalImports=res)
1109        self.importsDiagram.show()
1110
1111    def __showPackageDiagram(self):
1112        """
1113        Private method to handle the package diagram context menu action.
1114        """
1115        itm = self.model().item(self.currentIndex())
1116        try:
1117            fn = itm.fileName()
1118        except AttributeError:
1119            fn = itm.dirName()
1120        package = fn if os.path.isdir(fn) else os.path.dirname(fn)
1121        res = E5MessageBox.yesNo(
1122            self,
1123            self.tr("Package Diagram"),
1124            self.tr("""Include class attributes?"""),
1125            yesDefault=True)
1126
1127        from Graphics.UMLDialog import UMLDialog, UMLDialogType
1128        self.packageDiagram = UMLDialog(
1129            UMLDialogType.PACKAGE_DIAGRAM, self.project, package,
1130            self, noAttrs=not res)
1131        self.packageDiagram.show()
1132
1133    def __showApplicationDiagram(self):
1134        """
1135        Private method to handle the application diagram context menu action.
1136        """
1137        res = E5MessageBox.yesNo(
1138            self,
1139            self.tr("Application Diagram"),
1140            self.tr("""Include module names?"""),
1141            yesDefault=True)
1142
1143        from Graphics.UMLDialog import UMLDialog, UMLDialogType
1144        self.applicationDiagram = UMLDialog(
1145            UMLDialogType.APPLICATION_DIAGRAM, self.project,
1146            self, noModules=not res)
1147        self.applicationDiagram.show()
1148
1149    def __loadDiagram(self):
1150        """
1151        Private slot to load a diagram from file.
1152        """
1153        from Graphics.UMLDialog import UMLDialog, UMLDialogType
1154        self.loadedDiagram = None
1155        loadedDiagram = UMLDialog(
1156            UMLDialogType.NO_DIAGRAM, self.project, parent=self)
1157        if loadedDiagram.load():
1158            self.loadedDiagram = loadedDiagram
1159            self.loadedDiagram.show(fromFile=True)
1160