src/eric7/Project/ProjectSourcesBrowser.py

branch
eric7-maintenance
changeset 9549
67295777d9fe
parent 9442
906485dcd210
parent 9535
8b5402794fb6
child 9654
7328efba128b
--- 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."""),
+            )

eric ide

mercurial