--- a/src/eric7/Project/ProjectSourcesBrowser.py Mon Oct 31 14:07:57 2022 +0100 +++ b/src/eric7/Project/ProjectSourcesBrowser.py Wed Nov 30 09:19:51 2022 +0100 @@ -7,36 +7,38 @@ Module implementing a class used to display the Sources part of the project. """ +import contextlib import os -import contextlib from PyQt6.QtCore import pyqtSignal from PyQt6.QtWidgets import QDialog, QInputDialog, QMenu +from eric7 import Utilities +from eric7.CodeFormatting.BlackFormattingAction import BlackFormattingAction +from eric7.CodeFormatting.BlackUtilities import aboutBlack +from eric7.CodeFormatting.IsortFormattingAction import IsortFormattingAction +from eric7.CodeFormatting.IsortUtilities import aboutIsort +from eric7.EricGui import EricPixmapCache from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp - +from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType from eric7.UI.BrowserModel import ( + BrowserClassAttributeItem, + BrowserClassItem, BrowserFileItem, - BrowserClassItem, + BrowserImportItem, BrowserMethodItem, - BrowserClassAttributeItem, - BrowserImportItem, ) +from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog +from .FileCategoryRepositoryItem import FileCategoryRepositoryItem +from .ProjectBaseBrowser import ProjectBaseBrowser from .ProjectBrowserModel import ( + ProjectBrowserDirectoryItem, ProjectBrowserFileItem, ProjectBrowserSimpleDirectoryItem, - ProjectBrowserDirectoryItem, - ProjectBrowserSourceType, ) -from .ProjectBaseBrowser import ProjectBaseBrowser - -from eric7 import Utilities -from eric7.EricGui import EricPixmapCache - -from eric7.CodeFormatting.BlackFormattingAction import BlackFormattingAction -from eric7.CodeFormatting.BlackUtilities import aboutBlack +from .ProjectBrowserRepositoryItem import ProjectBrowserRepositoryItem class ProjectSourcesBrowser(ProjectBaseBrowser): @@ -49,14 +51,18 @@ showMenu = pyqtSignal(str, QMenu) - def __init__(self, project, parent=None): + def __init__(self, project, projectBrowser, parent=None): """ Constructor @param project reference to the project object - @param parent parent widget of this browser (QWidget) + @type Project + @param projectBrowser reference to the project browser object + @type ProjectBrowser + @param parent parent widget of this browser + @type QWidget """ - ProjectBaseBrowser.__init__(self, project, ProjectBrowserSourceType, parent) + ProjectBaseBrowser.__init__(self, project, "source", parent) self.selectedItemsFilter = [ ProjectBrowserFileItem, @@ -74,8 +80,49 @@ ) ) + # Add the file category handled by the browser. + project.addFileCategory( + "SOURCES", + FileCategoryRepositoryItem( + fileCategoryFilterTemplate=self.tr("Source Files ({0})"), + fileCategoryUserString=self.tr("Source Files"), + fileCategoryTyeString=self.tr("Sources"), + fileCategoryExtensions=["*.py", "*.pyw"], # Python files as default + ), + ) + + # Add the project browser type to the browser type repository. + projectBrowser.addTypedProjectBrowser( + "sources", + ProjectBrowserRepositoryItem( + projectBrowser=self, + projectBrowserUserString=self.tr("Sources Browser"), + priority=100, + fileCategory="SOURCES", + fileFilter="source", + getIcon=self.getIcon, + ), + ) + + # Connect signals of Project. project.prepareRepopulateItem.connect(self._prepareRepopulateItem) project.completeRepopulateItem.connect(self._completeRepopulateItem) + project.projectClosed.connect(self._projectClosed) + project.projectOpened.connect(self._projectOpened) + project.newProject.connect(self._newProject) + project.reinitVCS.connect(self._initMenusAndVcs) + project.projectPropertiesChanged.connect(self._initMenusAndVcs) + + # Connect signals of ProjectBrowser. + projectBrowser.preferencesChanged.connect(self.handlePreferencesChanged) + + # Connect some of our own signals. + self.sourceFile[str].connect(projectBrowser.sourceFile[str]) + self.sourceFile[str, int].connect(projectBrowser.sourceFile[str, int]) + self.sourceFile[str, list].connect(projectBrowser.sourceFile[str, list]) + self.sourceFile[str, int, str].connect(projectBrowser.sourceFile[str, int, str]) + self.closeSourceWindow.connect(projectBrowser.closeSourceWindow) + self.testFile.connect(projectBrowser.testFile) self.codemetrics = None self.codecoverage = None @@ -86,6 +133,35 @@ self.applicationDiagram = None self.loadedDiagram = None + def getIcon(self): + """ + Public method to get an icon for the project browser. + + @return icon for the browser + @rtype QIcon + """ + if not self.project.isOpen(): + icon = EricPixmapCache.getIcon("projectSources") + else: + if self.project.getProjectLanguage() == "Python3": + if self.project.isMixedLanguageProject(): + icon = EricPixmapCache.getIcon("projectSourcesPyMixed") + else: + icon = EricPixmapCache.getIcon("projectSourcesPy") + elif self.project.getProjectLanguage() == "MicroPython": + icon = EricPixmapCache.getIcon("micropython") + elif self.project.getProjectLanguage() == "Ruby": + if self.project.isMixedLanguageProject(): + icon = EricPixmapCache.getIcon("projectSourcesRbMixed") + else: + icon = EricPixmapCache.getIcon("projectSourcesRb") + elif self.project.getProjectLanguage() == "JavaScript": + icon = EricPixmapCache.getIcon("projectSourcesJavaScript") + else: + icon = EricPixmapCache.getIcon("projectSources") + + return icon + def __closeAllWindows(self): """ Private method to close all project related windows. @@ -147,6 +223,20 @@ self.tr("Formatting Diff"), lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff), ) + self.formattingMenu.addSeparator() + act = self.formattingMenu.addAction(self.tr("isort"), aboutIsort) + font = act.font() + font.setBold(True) + act.setFont(font) + self.formattingMenu.addAction( + self.tr("Sort Imports"), + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Sort), + ) + self.formattingMenu.addAction( + self.tr("Imports Sorting Diff"), + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Diff), + ) + self.formattingMenu.addSeparator() self.formattingMenu.aboutToShow.connect(self.__showContextMenuFormatting) self.menuShow = QMenu(self.tr("Show")) @@ -263,10 +353,12 @@ self.attributeMenu.addSeparator() self.attributeMenu.addAction(self.tr("New package..."), self.__addNewPackage) self.attributeMenu.addAction( - self.tr("Add source files..."), self.project.addSourceFiles + self.tr("Add source files..."), + lambda: self.project.addFiles("SOURCES"), ) self.attributeMenu.addAction( - self.tr("Add source directory..."), self.project.addSourceDir + self.tr("Add source directory..."), + lambda: self.project.addDirectory("SOURCES"), ) self.attributeMenu.addSeparator() self.attributeMenu.addAction( @@ -281,10 +373,12 @@ self.backMenu = QMenu(self) self.backMenu.addAction(self.tr("New package..."), self.__addNewPackage) self.backMenu.addAction( - self.tr("Add source files..."), self.project.addSourceFiles + self.tr("Add source files..."), + lambda: self.project.addFiles("SOURCES"), ) self.backMenu.addAction( - self.tr("Add source directory..."), self.project.addSourceDir + self.tr("Add source directory..."), + lambda: self.project.addDirectory("SOURCES"), ) self.backMenu.addSeparator() self.backMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs) @@ -420,10 +514,12 @@ self.attributeMenu.addMenu(self.gotoMenu) self.attributeMenu.addSeparator() self.attributeMenu.addAction( - self.tr("Add source files..."), self.project.addSourceFiles + self.tr("Add source files..."), + lambda: self.project.addFiles("SOURCES"), ) self.attributeMenu.addAction( - self.tr("Add source directory..."), self.project.addSourceDir + self.tr("Add source directory..."), + lambda: self.project.addDirectory("SOURCES"), ) self.attributeMenu.addSeparator() self.attributeMenu.addAction( @@ -437,10 +533,12 @@ self.backMenu = QMenu(self) self.backMenu.addAction( - self.tr("Add source files..."), self.project.addSourceFiles + self.tr("Add source files..."), + lambda: self.project.addFiles("SOURCES"), ) self.backMenu.addAction( - self.tr("Add source directory..."), self.project.addSourceDir + self.tr("Add source directory..."), + lambda: self.project.addDirectory("SOURCES"), ) self.backMenu.addSeparator() self.backMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs) @@ -556,10 +654,12 @@ self.attributeMenu.addMenu(self.gotoMenu) self.attributeMenu.addSeparator() self.attributeMenu.addAction( - self.tr("Add source files..."), self.project.addSourceFiles + self.tr("Add source files..."), + lambda: self.project.addFiles("SOURCES"), ) self.attributeMenu.addAction( - self.tr("Add source directory..."), self.project.addSourceDir + self.tr("Add source directory..."), + lambda: self.project.addDirectory("SOURCES"), ) self.attributeMenu.addSeparator() self.attributeMenu.addAction( @@ -573,10 +673,12 @@ self.backMenu = QMenu(self) self.backMenu.addAction( - self.tr("Add source files..."), self.project.addSourceFiles + self.tr("Add source files..."), + lambda: self.project.addFiles("SOURCES"), ) self.backMenu.addAction( - self.tr("Add source directory..."), self.project.addSourceDir + self.tr("Add source directory..."), + lambda: self.project.addDirectory("SOURCES"), ) self.backMenu.addSeparator() self.backMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs) @@ -650,7 +752,7 @@ if not self.project.isOpen(): return - with contextlib.suppress(Exception): + with contextlib.suppress(Exception): # secok categories = self.getSelectedItemsCountCategorized( [ ProjectBrowserFileItem, @@ -866,6 +968,8 @@ """ Private method to add a new package to the project. """ + from .NewPythonPackageDialog import NewPythonPackageDialog + itm = self.model().item(self.currentIndex()) if isinstance( itm, (ProjectBrowserFileItem, BrowserClassItem, BrowserMethodItem) @@ -881,8 +985,6 @@ dn = self.project.getRelativePath(dn) if dn.startswith(os.sep): dn = dn[1:] - from .NewPythonPackageDialog import NewPythonPackageDialog - dlg = NewPythonPackageDialog(dn, self) if dlg.exec() == QDialog.DialogCode.Accepted: packageName = dlg.getData() @@ -973,8 +1075,6 @@ fn = self.project.getRelativePath(fn2) files.append(fn) - from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog - dlg = DeleteFilesConfirmationDialog( self.parent(), self.tr("Delete files"), @@ -1005,11 +1105,11 @@ """ Private method to handle the code metrics context menu action. """ + from eric7.DataViews.CodeMetricsDialog import CodeMetricsDialog + itm = self.model().item(self.currentIndex()) fn = itm.fileName() - from eric7.DataViews.CodeMetricsDialog import CodeMetricsDialog - self.codemetrics = CodeMetricsDialog() self.codemetrics.show() self.codemetrics.start(fn) @@ -1018,19 +1118,25 @@ """ Private method to handle the code coverage context menu action. """ + from eric7.DataViews.PyCoverageDialog import PyCoverageDialog + itm = self.model().item(self.currentIndex()) fn = itm.fileName() pfn = self.project.getMainScript(True) - files = set() + files = [] if pfn is not None: - files |= set(Utilities.getCoverageFileNames(pfn)) + files.extend( + [f for f in Utilities.getCoverageFileNames(pfn) if f not in files] + ) if fn is not None: - files |= set(Utilities.getCoverageFileNames(fn)) + files.extend( + [f for f in Utilities.getCoverageFileNames(fn) if f not in files] + ) - if list(files): + if files: if len(files) > 1: cfn, ok = QInputDialog.getItem( None, @@ -1047,8 +1153,6 @@ else: return - from eric7.DataViews.PyCoverageDialog import PyCoverageDialog - self.codecoverage = PyCoverageDialog() self.codecoverage.show() self.codecoverage.start(cfn, fn) @@ -1057,19 +1161,25 @@ """ Private method to handle the show profile data context menu action. """ + from eric7.DataViews.PyProfileDialog import PyProfileDialog + itm = self.model().item(self.currentIndex()) fn = itm.fileName() pfn = self.project.getMainScript(True) - files = set() + files = [] if pfn is not None: - files |= set(Utilities.getProfileFileNames(pfn)) + files.extend( + [f for f in Utilities.getProfileFileNames(pfn) if f not in files] + ) if fn is not None: - files |= set(Utilities.getProfileFileNames(fn)) + files.extend( + [f for f in Utilities.getProfileFileNames(fn) if f not in files] + ) - if list(files): + if files: if len(files) > 1: pfn, ok = QInputDialog.getItem( None, @@ -1086,8 +1196,6 @@ else: return - from eric7.DataViews.PyProfileDialog import PyProfileDialog - self.profiledata = PyProfileDialog() self.profiledata.show() self.profiledata.start(pfn, fn) @@ -1118,8 +1226,6 @@ yesDefault=True, ) - from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType - self.classDiagram = UMLDialog( UMLDialogType.CLASS_DIAGRAM, self.project, fn, self, noAttrs=not res ) @@ -1141,8 +1247,6 @@ self.tr("""Include imports from external modules?"""), ) - from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType - self.importsDiagram = UMLDialog( UMLDialogType.IMPORTS_DIAGRAM, self.project, @@ -1169,8 +1273,6 @@ yesDefault=True, ) - from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType - self.packageDiagram = UMLDialog( UMLDialogType.PACKAGE_DIAGRAM, self.project, package, self, noAttrs=not res ) @@ -1187,8 +1289,6 @@ yesDefault=True, ) - from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType - self.applicationDiagram = UMLDialog( UMLDialogType.APPLICATION_DIAGRAM, self.project, self, noModules=not res ) @@ -1271,15 +1371,7 @@ files = [ itm.fileName() - for itm in self.getSelectedItems( - [ - BrowserFileItem, - BrowserClassItem, - BrowserMethodItem, - BrowserClassAttributeItem, - BrowserImportItem, - ] - ) + for itm in self.getSelectedItems([BrowserFileItem]) if itm.isPython3File() ] if not files: @@ -1310,3 +1402,59 @@ self.tr("Code Formatting"), self.tr("""There are no files left for reformatting."""), ) + + def __performImportSortingWithIsort(self, action): + """ + Private method to sort the import statements of the selected project sources + using the 'isort' tool. + + Following actions are supported. + <ul> + <li>IsortFormattingAction.Sort - the import statement sorting is performed</li> + <li>IsortFormattingAction.Check - a check is performed, if import statement + resorting is necessary</li> + <li>IsortFormattingAction.Diff - a unified diff of potential import statement + changes is generated</li> + </ul> + + @param action sorting operation to be performed + @type IsortFormattingAction + """ + from eric7.CodeFormatting.IsortConfigurationDialog import ( + IsortConfigurationDialog, + ) + from eric7.CodeFormatting.IsortFormattingDialog import IsortFormattingDialog + + files = [ + itm.fileName() + for itm in self.getSelectedItems([BrowserFileItem]) + if itm.isPython3File() + ] + if not files: + # called for a directory + itm = self.model().item(self.currentIndex()) + dirName = itm.dirName() + files = [ + f + for f in self.project.getProjectFiles("SOURCES", normalized=True) + if f.startswith(dirName) + ] + + vm = ericApp().getObject("ViewManager") + files = [fn for fn in files if vm.checkFileDirty(fn)] + + if files: + dlg = IsortConfigurationDialog(withProject=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + config = dlg.getConfiguration() + + formattingDialog = IsortFormattingDialog( + config, files, project=self.project, action=action + ) + formattingDialog.exec() + else: + EricMessageBox.information( + self, + self.tr("Import Sorting"), + self.tr("""There are no files left for import statement sorting."""), + )