src/eric7/Project/Project.py

branch
eric7-maintenance
changeset 9549
67295777d9fe
parent 9442
906485dcd210
parent 9533
e017c0df9ef1
child 9551
40e2dd39e687
--- a/src/eric7/Project/Project.py	Mon Oct 31 14:07:57 2022 +0100
+++ b/src/eric7/Project/Project.py	Wed Nov 30 09:19:51 2022 +0100
@@ -7,6 +7,7 @@
 Module implementing the project management functionality.
 """
 
+import collections
 import contextlib
 import copy
 import fnmatch
@@ -18,46 +19,48 @@
 import time
 import zipfile
 
+from PyQt6.Qsci import QsciScintilla
 from PyQt6.QtCore import (
-    pyqtSlot,
+    QByteArray,
+    QCryptographicHash,
     QFile,
-    pyqtSignal,
-    QCryptographicHash,
     QIODevice,
-    QByteArray,
     QObject,
     QProcess,
+    pyqtSignal,
+    pyqtSlot,
 )
-from PyQt6.QtGui import QKeySequence, QAction
-from PyQt6.QtWidgets import QLineEdit, QToolBar, QDialog, QInputDialog, QMenu
-from PyQt6.Qsci import QsciScintilla
-
+from PyQt6.QtGui import QAction, QKeySequence
+from PyQt6.QtWidgets import QDialog, QInputDialog, QLineEdit, QMenu, QToolBar
+
+from eric7 import Globals, Preferences, 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.EricGui.EricAction import EricAction, createActionGroup
+from eric7.EricGui.EricOverrideCursor import EricOverrideCursor, EricOverridenCursor
+from eric7.EricWidgets import EricFileDialog, EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
-from eric7.EricWidgets import EricFileDialog, EricMessageBox
 from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog
 from eric7.EricWidgets.EricProgressDialog import EricProgressDialog
-from eric7.EricGui.EricOverrideCursor import EricOverrideCursor, EricOverridenCursor
-
+from eric7.EricXML.DebuggerPropertiesReader import DebuggerPropertiesReader
+from eric7.EricXML.ProjectReader import ProjectReader
+from eric7.EricXML.SessionReader import SessionReader
+from eric7.EricXML.TasksReader import TasksReader
+from eric7.EricXML.UserProjectReader import UserProjectReader
 from eric7.Globals import recentNameProject
-
-from eric7.EricGui import EricPixmapCache
+from eric7.Sessions.SessionFile import SessionFile
+from eric7.Tasks.TasksFile import TasksFile
 from eric7.UI import Config
 from eric7.UI.NotificationWidget import NotificationTypes
 
-from eric7.EricGui.EricAction import EricAction, createActionGroup
-
-from eric7 import Globals, Preferences, Utilities
-
+from .DebuggerPropertiesFile import DebuggerPropertiesFile
+from .FileCategoryRepositoryItem import FileCategoryRepositoryItem
+from .ProjectBrowserModel import ProjectBrowserModel
 from .ProjectFile import ProjectFile
 from .UserProjectFile import UserProjectFile
-from .DebuggerPropertiesFile import DebuggerPropertiesFile
-
-from eric7.Sessions.SessionFile import SessionFile
-
-from eric7.Tasks.TasksFile import TasksFile
-
-from eric7.CodeFormatting.BlackFormattingAction import BlackFormattingAction
-from eric7.CodeFormatting.BlackUtilities import aboutBlack
 
 
 class Project(QObject):
@@ -65,26 +68,11 @@
     Class implementing the project management functionality.
 
     @signal dirty(bool) emitted when the dirty state changes
-    @signal projectLanguageAdded(str) emitted after a new language was added
+    @signal projectFileAdded(str, str) emitted after a new file was added
+    @signal projectFileRemoved(str, str) emitted after a file of the project was removed
+    @signal projectFileCompiled(str, str) emitted after a form was compiled
     @signal projectLanguageAddedByCode(str) emitted after a new language was
         added. The language code is sent by this signal.
-    @signal projectLanguageRemoved(str) emitted after a language was removed
-    @signal projectFormAdded(str) emitted after a new form was added
-    @signal projectFormRemoved(str) emitted after a form was removed
-    @signal projectFormCompiled(str) emitted after a form was compiled
-    @signal projectSourceAdded(str) emitted after a new source file was added
-    @signal projectSourceRemoved(str) emitted after a source was removed
-    @signal projectInterfaceAdded(str) emitted after a new IDL file was added
-    @signal projectInterfaceRemoved(str) emitted after a IDL file was removed
-    @signal projectProtocolAdded(str) emitted after a new proto file was added
-    @signal projectProtocolRemoved(str) emitted after a proto file was removed
-    @signal projectResourceAdded(str) emitted after a new resource file was
-        added
-    @signal projectResourceRemoved(str) emitted after a resource was removed
-    @signal projectOthersAdded(str) emitted after a file or directory was added
-        to the OTHERS project data area
-    @signal projectOthersRemoved(str) emitted after a file was removed from the
-        OTHERS project data area
     @signal projectAboutToBeCreated() emitted just before the project will be
         created
     @signal newProjectHooks() emitted after a new project was generated but
@@ -129,25 +117,15 @@
         a QProcess on stdout
     @signal appendStderr(str) emitted after something was received from
         a QProcess on stderr
+    @signal processChangedProjectFiles() emitted to indicate, that changed project files
+        should be processed
     """
 
     dirty = pyqtSignal(bool)
-    projectLanguageAdded = pyqtSignal(str)
+    projectFileAdded = pyqtSignal(str, str)
+    projectFileRemoved = pyqtSignal(str, str)
+    projectFileCompiled = pyqtSignal(str, str)
     projectLanguageAddedByCode = pyqtSignal(str)
-    projectLanguageRemoved = pyqtSignal(str)
-    projectFormAdded = pyqtSignal(str)
-    projectFormRemoved = pyqtSignal(str)
-    projectFormCompiled = pyqtSignal(str)
-    projectSourceAdded = pyqtSignal(str)
-    projectSourceRemoved = pyqtSignal(str)
-    projectInterfaceAdded = pyqtSignal(str)
-    projectInterfaceRemoved = pyqtSignal(str)
-    projectProtocolAdded = pyqtSignal(str)
-    projectProtocolRemoved = pyqtSignal(str)
-    projectResourceAdded = pyqtSignal(str)
-    projectResourceRemoved = pyqtSignal(str)
-    projectOthersAdded = pyqtSignal(str)
-    projectOthersRemoved = pyqtSignal(str)
     projectAboutToBeCreated = pyqtSignal()
     newProjectHooks = pyqtSignal()
     newProject = pyqtSignal()
@@ -174,6 +152,7 @@
     projectChanged = pyqtSignal()
     appendStdout = pyqtSignal(str)
     appendStderr = pyqtSignal(str)
+    processChangedProjectFiles = pyqtSignal()
 
     eols = [os.linesep, "\n", "\r", "\r\n"]
 
@@ -204,6 +183,11 @@
             ),
         }
 
+        self.__fileCategoriesRepository = {}
+        # This dictionary will be populated by the various project browsers with
+        # classes of type 'FileCategoryRepositoryItem' using the 'addFileCategory()
+        # and removeFileCategory() methods.
+
         self.vcsMenu = None
         self.__makeProcess = None
 
@@ -225,8 +209,6 @@
         else:
             self.vcs = self.initVCS()
 
-        from .ProjectBrowserModel import ProjectBrowserModel
-
         self.__model = ProjectBrowserModel(self)
 
         self.codemetrics = None
@@ -236,6 +218,52 @@
         self.loadedDiagram = None
         self.__findProjectFileDialog = None
 
+        self.processChangedProjectFiles.connect(self.__autoExecuteMake)
+
+    def addFileCategory(self, category, categoryItem):
+        """
+        Public method to add a file category to the categories repository.
+
+        Note: The given category must not be contained in the repository already.
+
+        @param category file category (must be unique)
+        @type str
+        @param categoryItem data class instance containing the category data
+        @type FileCategoryRepositoryItem
+        @exception TypeError raised to signal a wrong type for the category item
+        """
+        if not isinstance(categoryItem, FileCategoryRepositoryItem):
+            raise TypeError(
+                "'categoryItem' must be an instance of 'FileCategoryRepositoryItem'."
+            )
+
+        if category in self.__fileCategoriesRepository:
+            EricMessageBox.critical(
+                self.ui,
+                self.tr("Add File Category"),
+                self.tr(
+                    "<p>The file category <b>{0}</b> has already been added. This"
+                    " attempt will be ignored.</p>"
+                ),
+            )
+        else:
+            self.__fileCategoriesRepository[category] = categoryItem
+            with contextlib.suppress(AttributeError):
+                self.__pdata[category] = []
+
+    def removeFileCategory(self, category):
+        """
+        Public method to remove a category from the categories repository.
+
+        Note: If the category is not contained in the repository, the request to
+        remove it will be ignored silently.
+
+        @param category file category
+        @type str
+        """
+        with contextlib.suppress(KeyError):
+            del self.__fileCategoriesRepository[category]
+
     def __sourceExtensions(self, language):
         """
         Private method to get the source extensions of a programming language.
@@ -484,16 +512,9 @@
             "redirect": True,
         }
 
-        self.pdata = {
+        self.__pdata = {
             "DESCRIPTION": "",
             "VERSION": "",
-            "SOURCES": [],
-            "FORMS": [],
-            "RESOURCES": [],
-            "INTERFACES": [],
-            "PROTOCOLS": [],
-            "OTHERS": [],
-            "TRANSLATIONS": [],
             "TRANSLATIONEXCEPTIONS": [],
             "TRANSLATIONPATTERN": "",
             "TRANSLATIONSBINPATH": "",
@@ -548,6 +569,8 @@
             "LICENSE": "",
             "EMBEDDED_VENV": False,
         }
+        for category in self.__fileCategoriesRepository:
+            self.__pdata[category] = []
 
         self.__initDebugProperties()
 
@@ -560,6 +583,52 @@
 
         self.__initVenvConfiguration()
 
+    def getProjectData(self, dataKey=None, default=None):
+        """
+        Public method to get the data associated with the given data key.
+
+        Note: If dataKey is None, a copy of the project data structure
+        is returned.
+
+        @param dataKey key of the data to get (defaults to None)
+        @type str (optional)
+        @param default default value for non-existent keys (defaults to None)
+        @type Any (optional)
+        @return requested data or None if the data key doesn't exist or
+            a copy of the project data dictionary
+        @rtype Any
+        """
+        if dataKey is None:
+            return copy.deepcopy(self.__pdata)
+
+        try:
+            return self.__pdata[dataKey]
+        except KeyError:
+            return default
+
+    def setProjectData(self, data, dataKey=None, setDirty=True):
+        """
+        Public method to set data associated with the given data key in the project
+        dictionary.
+
+        Note: If no data key is given or is None, the data must be a dictionary used
+        to update the project data.
+
+        @param data data to be set or a dictionary to update the project data
+        @type Any
+        @param dataKey key of the data to set (defaults to None)
+        @type str (optional)
+        @param setDirty flag indicating to set the dirty flag if the data is different
+            from the current one (defaults to True)
+        @type bool (optional)
+        """
+        if dataKey is None:
+            self.__pdata.update(data)
+        else:
+            if self.__pdata[dataKey] != data and setDirty:
+                self.setDirty(True)
+            self.__pdata[dataKey] = data
+
     def getData(self, category, key):
         """
         Public method to get data out of the project data store.
@@ -580,9 +649,9 @@
                 "DOCUMENTATIONPARMS",
                 "OTHERTOOLSPARMS",
             ]
-            and key in self.pdata[category]
+            and key in self.__pdata[category]
         ):
-            return copy.deepcopy(self.pdata[category][key])
+            return copy.deepcopy(self.__pdata[category][key])
         else:
             return None
 
@@ -609,29 +678,124 @@
 
         # test for changes of data and save them in the project
         # 1. there were none, now there are
-        if key not in self.pdata[category] and len(data) > 0:
-            self.pdata[category][key] = copy.deepcopy(data)
+        if key not in self.__pdata[category] and len(data) > 0:
+            self.__pdata[category][key] = copy.deepcopy(data)
             self.setDirty(True)
         # 2. there were some, now there aren't
-        elif key in self.pdata[category] and len(data) == 0:
-            del self.pdata[category][key]
+        elif key in self.__pdata[category] and len(data) == 0:
+            del self.__pdata[category][key]
             self.setDirty(True)
         # 3. there were some and still are
-        elif key in self.pdata[category] and len(data) > 0:
-            if data != self.pdata[category][key]:
-                self.pdata[category][key] = copy.deepcopy(data)
+        elif key in self.__pdata[category] and len(data) > 0:
+            if data != self.__pdata[category][key]:
+                self.__pdata[category][key] = copy.deepcopy(data)
                 self.setDirty(True)
         # 4. there were none and none are given
         else:
             return False
         return True
 
+    def getFileCategories(self):
+        """
+        Public method to get the list of known file categories.
+
+        @return list of known file categories
+        @rtype list of str
+        """
+        return list(self.__fileCategoriesRepository.keys())
+
+    def getFileCategoryFilterString(
+        self, categories=None, withOthers=False, withAll=True
+    ):
+        """
+        Public method to get a file selection string for the given categories.
+
+        @param categories list of file type categories (defaults to None).
+            A value of None means all categories except 'OTHERS'.
+        @type list of str (optional)
+        @param withOthers flag indicating to include the 'OTHERS' category
+            (defaults to False)
+        @type bool (optional)
+        @param withAll flag indicating to include a filter for 'All Files'
+            (defaults to True)
+        @type bool (optional)
+        @return file selection filter string
+        @rtype str
+        """
+        if categories is None:
+            categories = [c for c in self.__fileCategoriesRepository if c != "OTHERS"]
+            if withOthers:
+                categories.append("OTHERS")
+
+        patterns = collections.defaultdict(list)
+        for pattern, filetype in self.__pdata["FILETYPES"].items():
+            if filetype in categories and filetype in self.__fileCategoriesRepository:
+                patterns[filetype].append(pattern)
+
+        filters = []
+        for filetype in patterns:
+            filters.append(
+                self.__fileCategoriesRepository[
+                    filetype
+                ].fileCategoryFilterTemplate.format(
+                    " ".join(sorted(patterns[filetype]))
+                )
+            )
+        filterString = ";;".join(sorted(filters))
+        if withAll:
+            filterString += ";;" + self.tr("All Files (*)")
+
+        return filterString
+
+    def getFileCategoryString(self, category):
+        """
+        Public method to get a user string for the given category.
+
+        @param category file type category
+        @type str
+        @return user string for the category
+        @rtype str
+        """
+        return self.__fileCategoriesRepository[category].fileCategoryUserString
+
+    def getFileCategoryType(self, category):
+        """
+        Public method to get a user type string for the given category.
+
+        @param category file type category
+        @type str
+        @return user type string for the category
+        @rtype str
+        """
+        return self.__fileCategoriesRepository[category].fileCategoryTyeString
+
+    def getFileCategoryExtension(self, category, reverse=False):
+        """
+        Public method to get a list of default file extensions for the given category.
+
+        @param category file type category
+        @type str
+        @param reverse flag indicating to get all other extensions except the one of
+            the given category
+        @type bool
+        @return list of default file extensions for the category
+        @rtype list of str
+        """
+        if reverse:
+            extensions = []
+            for cat, item in self.__fileCategoriesRepository.items():
+                if cat != category:
+                    extensions += item.fileCategoryExtensions[:]
+            return extensions
+        else:
+            return self.__fileCategoriesRepository[category].fileCategoryExtensions[:]
+
     def initFileTypes(self):
         """
         Public method to initialize the filetype associations with default
         values.
         """
-        self.pdata["FILETYPES"] = {
+        self.__pdata["FILETYPES"] = {
             "*.txt": "OTHERS",
             "*.md": "OTHERS",
             "*.rst": "OTHERS",
@@ -646,29 +810,29 @@
 
         # Sources
         sourceKey = (
-            "Mixed" if self.pdata["MIXEDLANGUAGE"] else self.pdata["PROGLANGUAGE"]
+            "Mixed" if self.__pdata["MIXEDLANGUAGE"] else self.__pdata["PROGLANGUAGE"]
         )
         for ext in self.__sourceExtensions(sourceKey):
-            self.pdata["FILETYPES"]["*{0}".format(ext)] = "SOURCES"
+            self.__pdata["FILETYPES"]["*{0}".format(ext)] = "SOURCES"
 
         # IDL interfaces
-        self.pdata["FILETYPES"]["*.idl"] = "INTERFACES"
+        self.__pdata["FILETYPES"]["*.idl"] = "INTERFACES"
 
         # Protobuf Files
-        self.pdata["FILETYPES"]["*.proto"] = "PROTOCOLS"
+        self.__pdata["FILETYPES"]["*.proto"] = "PROTOCOLS"
 
         # Forms
-        if self.pdata["PROJECTTYPE"] in [
+        if self.__pdata["PROJECTTYPE"] in [
             "E7Plugin",
             "PyQt5",
             "PyQt6",
             "PySide2",
             "PySide6",
         ]:
-            self.pdata["FILETYPES"]["*.ui"] = "FORMS"
+            self.__pdata["FILETYPES"]["*.ui"] = "FORMS"
 
         # Resources
-        if self.pdata["PROJECTTYPE"] in [
+        if self.__pdata["PROJECTTYPE"] in [
             "PyQt5",
             "PyQt5C",
             "PySide2",
@@ -676,10 +840,10 @@
             "PySide6",
             "PySide6C",
         ]:
-            self.pdata["FILETYPES"]["*.qrc"] = "RESOURCES"
+            self.__pdata["FILETYPES"]["*.qrc"] = "RESOURCES"
 
         # Translations
-        if self.pdata["PROJECTTYPE"] in [
+        if self.__pdata["PROJECTTYPE"] in [
             "E7Plugin",
             "PyQt5",
             "PyQt5C",
@@ -690,14 +854,14 @@
             "PySide6",
             "PySide6C",
         ]:
-            self.pdata["FILETYPES"]["*.ts"] = "TRANSLATIONS"
-            self.pdata["FILETYPES"]["*.qm"] = "TRANSLATIONS"
+            self.__pdata["FILETYPES"]["*.ts"] = "TRANSLATIONS"
+            self.__pdata["FILETYPES"]["*.qm"] = "TRANSLATIONS"
 
         # Project type specific ones
         with contextlib.suppress(KeyError):
-            if self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]] is not None:
-                ftypes = self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]]()
-                self.pdata["FILETYPES"].update(ftypes)
+            if self.__fileTypeCallbacks[self.__pdata["PROJECTTYPE"]] is not None:
+                ftypes = self.__fileTypeCallbacks[self.__pdata["PROJECTTYPE"]]()
+                self.__pdata["FILETYPES"].update(ftypes)
 
         self.setDirty(True)
 
@@ -706,7 +870,7 @@
         Public method to update the filetype associations with new default
         values.
         """
-        if self.pdata["PROJECTTYPE"] in [
+        if self.__pdata["PROJECTTYPE"] in [
             "E7Plugin",
             "PyQt5",
             "PyQt5C",
@@ -717,16 +881,16 @@
             "PySide6",
             "PySide6C",
         ]:
-            if "*.ts" not in self.pdata["FILETYPES"]:
-                self.pdata["FILETYPES"]["*.ts"] = "TRANSLATIONS"
-            if "*.qm" not in self.pdata["FILETYPES"]:
-                self.pdata["FILETYPES"]["*.qm"] = "TRANSLATIONS"
+            if "*.ts" not in self.__pdata["FILETYPES"]:
+                self.__pdata["FILETYPES"]["*.ts"] = "TRANSLATIONS"
+            if "*.qm" not in self.__pdata["FILETYPES"]:
+                self.__pdata["FILETYPES"]["*.qm"] = "TRANSLATIONS"
         with contextlib.suppress(KeyError):
-            if self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]] is not None:
-                ftypes = self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]]()
+            if self.__fileTypeCallbacks[self.__pdata["PROJECTTYPE"]] is not None:
+                ftypes = self.__fileTypeCallbacks[self.__pdata["PROJECTTYPE"]]()
                 for pattern, ftype in list(ftypes.items()):
-                    if pattern not in self.pdata["FILETYPES"]:
-                        self.pdata["FILETYPES"][pattern] = ftype
+                    if pattern not in self.__pdata["FILETYPES"]:
+                        self.__pdata["FILETYPES"][pattern] = ftype
                         self.setDirty(True)
 
     def __loadRecent(self):
@@ -845,14 +1009,14 @@
         """
         removed = False
         removelist = []
-        for file in self.pdata[index]:
+        for file in self.__pdata[index]:
             if not os.path.exists(os.path.join(self.ppath, file)):
                 removelist.append(file)
                 removed = True
 
         if removed:
             for file in removelist:
-                self.pdata[index].remove(file)
+                self.__pdata[index].remove(file)
             self.setDirty(True)
 
     def __readProject(self, fn):
@@ -870,15 +1034,13 @@
             # old XML based format
             f = QFile(fn)
             if f.open(QIODevice.OpenModeFlag.ReadOnly):
-                from eric7.EricXML.ProjectReader import ProjectReader
-
                 reader = ProjectReader(f, self)
                 reader.readXML()
                 res = not reader.hasError()
                 f.close()
 
                 # create hash value, if it doesn't have one
-                if reader.version.startswith("5.") and not self.pdata["HASH"]:
+                if reader.version.startswith("5.") and not self.__pdata["HASH"]:
                     hashStr = str(
                         QCryptographicHash.hash(
                             QByteArray(self.ppath.encode("utf-8")),
@@ -886,7 +1048,7 @@
                         ).toHex(),
                         encoding="utf-8",
                     )
-                    self.pdata["HASH"] = hashStr
+                    self.__pdata["HASH"] = hashStr
                     self.setDirty(True)
             else:
                 EricMessageBox.critical(
@@ -905,12 +1067,12 @@
             # insert filename into list of recently opened projects
             self.__syncRecent()
 
-            if self.pdata["TRANSLATIONPATTERN"]:
-                self.translationsRoot = self.pdata["TRANSLATIONPATTERN"].split(
+            if self.__pdata["TRANSLATIONPATTERN"]:
+                self.translationsRoot = self.__pdata["TRANSLATIONPATTERN"].split(
                     "%language%"
                 )[0]
-            elif self.pdata["MAINSCRIPT"]:
-                self.translationsRoot = os.path.splitext(self.pdata["MAINSCRIPT"])[0]
+            elif self.__pdata["MAINSCRIPT"]:
+                self.translationsRoot = os.path.splitext(self.__pdata["MAINSCRIPT"])[0]
             if os.path.isdir(os.path.join(self.ppath, self.translationsRoot)):
                 dn = self.translationsRoot
             else:
@@ -922,29 +1084,18 @@
 
             # check, if the files of the project still exist in the
             # project directory
-            self.__checkFilesExist("SOURCES")
-            self.__checkFilesExist("FORMS")
-            self.__checkFilesExist("INTERFACES")
-            self.__checkFilesExist("PROTOCOLS")
-            self.__checkFilesExist("TRANSLATIONS")
-            self.__checkFilesExist("RESOURCES")
-            self.__checkFilesExist("OTHERS")
+            for fileCategory in self.getFileCategories():
+                self.__checkFilesExist(fileCategory)
 
             # get the names of subdirectories the files are stored in
-            for fn in (
-                self.pdata["SOURCES"]
-                + self.pdata["FORMS"]
-                + self.pdata["INTERFACES"]
-                + self.pdata["PROTOCOLS"]
-                + self.pdata["RESOURCES"]
-                + self.pdata["TRANSLATIONS"]
-            ):
-                dn = os.path.dirname(fn)
-                if dn not in self.subdirs:
-                    self.subdirs.append(dn)
+            for fileCategory in [c for c in self.getFileCategories() if c != "OTHERS"]:
+                for fn in self.__pdata[fileCategory]:
+                    dn = os.path.dirname(fn)
+                    if dn not in self.subdirs:
+                        self.subdirs.append(dn)
 
             # get the names of other subdirectories
-            for fn in self.pdata["OTHERS"]:
+            for fn in self.__pdata["OTHERS"]:
                 dn = os.path.dirname(fn)
                 if dn not in self.otherssubdirs:
                     self.otherssubdirs.append(dn)
@@ -963,10 +1114,10 @@
         @return flag indicating success
         """
         if self.vcs is not None:
-            self.pdata["VCSOPTIONS"] = copy.deepcopy(self.vcs.vcsGetOptions())
-            self.pdata["VCSOTHERDATA"] = copy.deepcopy(self.vcs.vcsGetOtherData())
-
-        if not self.pdata["HASH"]:
+            self.__pdata["VCSOPTIONS"] = copy.deepcopy(self.vcs.vcsGetOptions())
+            self.__pdata["VCSOTHERDATA"] = copy.deepcopy(self.vcs.vcsGetOtherData())
+
+        if not self.__pdata["HASH"]:
             hashStr = str(
                 QCryptographicHash.hash(
                     QByteArray(self.ppath.encode("utf-8")),
@@ -974,7 +1125,7 @@
                 ).toHex(),
                 encoding="utf-8",
             )
-            self.pdata["HASH"] = hashStr
+            self.__pdata["HASH"] = hashStr
 
         if fn is None:
             fn = self.pfile
@@ -1012,8 +1163,6 @@
             if os.path.exists(fn):
                 f = QFile(fn)
                 if f.open(QIODevice.OpenModeFlag.ReadOnly):
-                    from eric7.EricXML.UserProjectReader import UserProjectReader
-
                     reader = UserProjectReader(f, self)
                     reader.readXML()
                     f.close()
@@ -1088,8 +1237,6 @@
             if os.path.exists(fn):
                 f = QFile(fn)
                 if f.open(QIODevice.OpenModeFlag.ReadOnly):
-                    from eric7.EricXML.SessionReader import SessionReader
-
                     reader = SessionReader(f, False)
                     reader.readXML(quiet=quiet)
                     f.close()
@@ -1183,8 +1330,6 @@
             if os.path.exists(fn):
                 f = QFile(fn)
                 if f.open(QIODevice.OpenModeFlag.ReadOnly):
-                    from eric7.EricXML.TasksReader import TasksReader
-
                     reader = TasksReader(f, True)
                     reader.readXML()
                     f.close()
@@ -1262,10 +1407,6 @@
 
             f = QFile(fn)
             if f.open(QIODevice.OpenModeFlag.ReadOnly):
-                from eric7.EricXML.DebuggerPropertiesReader import (
-                    DebuggerPropertiesReader,
-                )
-
                 reader = DebuggerPropertiesReader(f, self)
                 reader.readXML(quiet=quiet)
                 f.close()
@@ -1366,7 +1507,7 @@
 
         @return load status of debug properties (boolean)
         """
-        return self.debugPropertiesLoaded or self.pdata["EMBEDDED_VENV"]
+        return self.debugPropertiesLoaded or self.__pdata["EMBEDDED_VENV"]
 
     def __showDebugProperties(self):
         """
@@ -1471,7 +1612,7 @@
 
         @return translation pattern (string)
         """
-        return self.pdata["TRANSLATIONPATTERN"]
+        return self.__pdata["TRANSLATIONPATTERN"]
 
     def setTranslationPattern(self, pattern):
         """
@@ -1480,13 +1621,15 @@
         @param pattern translation pattern
         @type str
         """
-        self.pdata["TRANSLATIONPATTERN"] = pattern
+        self.__pdata["TRANSLATIONPATTERN"] = pattern
 
     def addLanguage(self):
         """
         Public slot used to add a language to the project.
         """
-        if not self.pdata["TRANSLATIONPATTERN"]:
+        from .AddLanguageDialog import AddLanguageDialog
+
+        if not self.__pdata["TRANSLATIONPATTERN"]:
             EricMessageBox.critical(
                 self.ui,
                 self.tr("Add Language"),
@@ -1494,12 +1637,10 @@
             )
             return
 
-        from .AddLanguageDialog import AddLanguageDialog
-
         dlg = AddLanguageDialog(self.parent())
         if dlg.exec() == QDialog.DialogCode.Accepted:
             lang = dlg.getSelectedLanguage()
-            if self.pdata["PROJECTTYPE"] in [
+            if self.__pdata["PROJECTTYPE"] in [
                 "PyQt5",
                 "PyQt5C",
                 "PyQt6",
@@ -1510,7 +1651,9 @@
                 "PySide6",
                 "PySide6C",
             ]:
-                langFile = self.pdata["TRANSLATIONPATTERN"].replace("%language%", lang)
+                langFile = self.__pdata["TRANSLATIONPATTERN"].replace(
+                    "%language%", lang
+                )
                 self.appendFile(langFile)
             self.projectLanguageAddedByCode.emit(lang)
 
@@ -1525,12 +1668,12 @@
         qmFile = ""
         try:
             if (
-                self.__binaryTranslationsCallbacks[self.pdata["PROJECTTYPE"]]
+                self.__binaryTranslationsCallbacks[self.__pdata["PROJECTTYPE"]]
                 is not None
             ):
-                qmFile = self.__binaryTranslationsCallbacks[self.pdata["PROJECTTYPE"]](
-                    langFile
-                )
+                qmFile = self.__binaryTranslationsCallbacks[
+                    self.__pdata["PROJECTTYPE"]
+                ](langFile)
         except KeyError:
             qmFile = langFile.replace(".ts", ".qm")
         if qmFile == langFile:
@@ -1541,17 +1684,17 @@
         """
         Public slot to check the language files after a release process.
         """
-        tbPath = self.pdata["TRANSLATIONSBINPATH"]
-        for langFile in self.pdata["TRANSLATIONS"][:]:
+        tbPath = self.__pdata["TRANSLATIONSBINPATH"]
+        for langFile in self.__pdata["TRANSLATIONS"][:]:
             qmFile = self.__binaryTranslationFile(langFile)
             if qmFile:
-                if qmFile not in self.pdata["TRANSLATIONS"] and os.path.exists(
+                if qmFile not in self.__pdata["TRANSLATIONS"] and os.path.exists(
                     os.path.join(self.ppath, qmFile)
                 ):
                     self.appendFile(qmFile)
                 if tbPath:
                     qmFile = os.path.join(tbPath, os.path.basename(qmFile))
-                    if qmFile not in self.pdata["TRANSLATIONS"] and os.path.exists(
+                    if qmFile not in self.__pdata["TRANSLATIONS"] and os.path.exists(
                         os.path.join(self.ppath, qmFile)
                     ):
                         self.appendFile(qmFile)
@@ -1566,17 +1709,18 @@
         """
         langFile = self.getRelativePath(langFile)
         qmFile = self.__binaryTranslationFile(langFile)
-        self.pdata["TRANSLATIONS"].remove(langFile)
+        self.__pdata["TRANSLATIONS"].remove(langFile)
         self.__model.removeItem(langFile)
         if qmFile:
             with contextlib.suppress(ValueError):
-                if self.pdata["TRANSLATIONSBINPATH"]:
+                if self.__pdata["TRANSLATIONSBINPATH"]:
                     qmFile = self.getRelativePath(
                         os.path.join(
-                            self.pdata["TRANSLATIONSBINPATH"], os.path.basename(qmFile)
+                            self.__pdata["TRANSLATIONSBINPATH"],
+                            os.path.basename(qmFile),
                         )
                     )
-                self.pdata["TRANSLATIONS"].remove(qmFile)
+                self.__pdata["TRANSLATIONS"].remove(qmFile)
                 self.__model.removeItem(qmFile)
         self.setDirty(True)
 
@@ -1587,7 +1731,7 @@
         @param langFile the translation file to be removed (string)
         """
         try:
-            from send2trash import send2trash as s2t
+            from send2trash import send2trash as s2t  # __IGNORE_WARNING_I10__
         except ImportError:
             s2t = os.remove
 
@@ -1614,10 +1758,11 @@
         # now get rid of the .qm file
         if qmFile:
             try:
-                if self.pdata["TRANSLATIONSBINPATH"]:
+                if self.__pdata["TRANSLATIONSBINPATH"]:
                     qmFile = self.getRelativePath(
                         os.path.join(
-                            self.pdata["TRANSLATIONSBINPATH"], os.path.basename(qmFile)
+                            self.__pdata["TRANSLATIONSBINPATH"],
+                            os.path.basename(qmFile),
                         )
                     )
                 fn = os.path.join(self.ppath, qmFile)
@@ -1659,68 +1804,39 @@
             if fnmatch.fnmatch(bfn, "*.ts") or fnmatch.fnmatch(bfn, "*.qm"):
                 filetype = "TRANSLATIONS"
             else:
-                for pattern in sorted(self.pdata["FILETYPES"].keys(), reverse=True):
+                for pattern in sorted(self.__pdata["FILETYPES"].keys(), reverse=True):
                     if fnmatch.fnmatch(bfn, pattern):
-                        filetype = self.pdata["FILETYPES"][pattern]
+                        filetype = self.__pdata["FILETYPES"][pattern]
                         break
 
         if filetype == "__IGNORE__":
             return
 
-        if filetype in ["SOURCES", "FORMS", "INTERFACES", "PROTOCOLS", "RESOURCES"]:
-            if filetype == "SOURCES":
-                if newfn not in self.pdata["SOURCES"]:
-                    self.pdata["SOURCES"].append(newfn)
-                    self.projectSourceAdded.emit(newfn)
-                    updateModel and self.__model.addNewItem("SOURCES", newfn)
-                    dirty = True
-                else:
-                    updateModel and self.repopulateItem(newfn)
-            elif filetype == "FORMS":
-                if newfn not in self.pdata["FORMS"]:
-                    self.pdata["FORMS"].append(newfn)
-                    self.projectFormAdded.emit(newfn)
-                    updateModel and self.__model.addNewItem("FORMS", newfn)
-                    dirty = True
-                else:
-                    updateModel and self.repopulateItem(newfn)
-            elif filetype == "INTERFACES":
-                if newfn not in self.pdata["INTERFACES"]:
-                    self.pdata["INTERFACES"].append(newfn)
-                    self.projectInterfaceAdded.emit(newfn)
-                    (updateModel and self.__model.addNewItem("INTERFACES", newfn))
-                    dirty = True
-                else:
-                    updateModel and self.repopulateItem(newfn)
-            elif filetype == "PROTOCOLS":
-                if newfn not in self.pdata["PROTOCOLS"]:
-                    self.pdata["PROTOCOLS"].append(newfn)
-                    self.projectProtocolAdded.emit(newfn)
-                    (updateModel and self.__model.addNewItem("PROTOCOLS", newfn))
-                    dirty = True
-                else:
-                    updateModel and self.repopulateItem(newfn)
-            elif filetype == "RESOURCES":
-                if newfn not in self.pdata["RESOURCES"]:
-                    self.pdata["RESOURCES"].append(newfn)
-                    self.projectResourceAdded.emit(newfn)
-                    updateModel and self.__model.addNewItem("RESOURCES", newfn)
-                    dirty = True
-                else:
-                    updateModel and self.repopulateItem(newfn)
+        if filetype in (
+            category
+            for category in self.getFileCategories()
+            if category not in ("TRANSLATIONS", "OTHERS")
+        ):
+            if newfn not in self.__pdata[filetype]:
+                self.__pdata[filetype].append(newfn)
+                self.projectFileAdded.emit(newfn, filetype)
+                updateModel and self.__model.addNewItem(filetype, newfn)
+                dirty = True
+            else:
+                updateModel and self.repopulateItem(newfn)
             if newdir not in self.subdirs:
                 self.subdirs.append(newdir)
         elif filetype == "TRANSLATIONS":
-            if newfn not in self.pdata["TRANSLATIONS"]:
-                self.pdata["TRANSLATIONS"].append(newfn)
+            if newfn not in self.__pdata["TRANSLATIONS"]:
+                self.__pdata["TRANSLATIONS"].append(newfn)
                 updateModel and self.__model.addNewItem("TRANSLATIONS", newfn)
-                self.projectLanguageAdded.emit(newfn)
+                self.projectFileAdded.emit(newfn, "TRANSLATIONS")
                 dirty = True
             else:
                 updateModel and self.repopulateItem(newfn)
-        else:  # filetype == "OTHERS"
-            if newfn not in self.pdata["OTHERS"]:
-                self.pdata["OTHERS"].append(newfn)
+        elif filetype == "OTHERS":
+            if newfn not in self.__pdata["OTHERS"]:
+                self.__pdata["OTHERS"].append(newfn)
                 self.othersAdded(newfn, updateModel)
                 dirty = True
             else:
@@ -1741,9 +1857,10 @@
         @param startdir start directory for the selection dialog
         @type str
         """
+        from .AddFileDialog import AddFileDialog
+
         if startdir is None:
             startdir = self.ppath
-        from .AddFileDialog import AddFileDialog
 
         dlg = AddFileDialog(self, self.parent(), fileTypeFilter, startdir=startdir)
         if dlg.exec() == QDialog.DialogCode.Accepted:
@@ -1803,7 +1920,7 @@
         # get all relevant filename patterns
         patterns = []
         ignorePatterns = []
-        for pattern, patterntype in list(self.pdata["FILETYPES"].items()):
+        for pattern, patterntype in list(self.__pdata["FILETYPES"].items()):
             if patterntype == filetype:
                 patterns.append(pattern)
             elif patterntype == "__IGNORE__":
@@ -1883,7 +2000,7 @@
 
         ignore_patterns = [
             pattern
-            for pattern, filetype in self.pdata["FILETYPES"].items()
+            for pattern, filetype in self.__pdata["FILETYPES"].items()
             if filetype == "__IGNORE__"
         ]
 
@@ -1912,9 +2029,10 @@
         @param startdir start directory for the selection dialog
         @type str
         """
+        from .AddDirectoryDialog import AddDirectoryDialog
+
         if startdir is None:
             startdir = self.ppath
-        from .AddDirectoryDialog import AddDirectoryDialog
 
         dlg = AddDirectoryDialog(self, fileTypeFilter, self.parent(), startdir=startdir)
         if dlg.exec() == QDialog.DialogCode.Accepted:
@@ -1958,90 +2076,14 @@
             if fn.endswith(os.sep):
                 fn = fn[:-1]
 
-            if fn not in self.pdata["OTHERS"]:
-                self.pdata["OTHERS"].append(fn)
+            if fn not in self.__pdata["OTHERS"]:
+                self.__pdata["OTHERS"].append(fn)
                 self.othersAdded(fn)
                 self.setDirty(True)
 
             if os.path.isdir(fn) and fn not in self.otherssubdirs:
                 self.otherssubdirs.append(fn)
 
-    def addSourceFiles(self):
-        """
-        Public slot to add source files to the current project.
-        """
-        self.addFiles("source")
-
-    def addUiFiles(self):
-        """
-        Public slot to add forms to the current project.
-        """
-        self.addFiles("form")
-
-    def addIdlFiles(self):
-        """
-        Public slot to add IDL interfaces to the current project.
-        """
-        self.addFiles("interface")
-
-    def addProtoFiles(self):
-        """
-        Public slot to add protocol files to the current project.
-        """
-        self.addFiles("protocol")
-
-    def addResourceFiles(self):
-        """
-        Public slot to add Qt resources to the current project.
-        """
-        self.addFiles("resource")
-
-    def addOthersFiles(self):
-        """
-        Public slot to add files to the OTHERS project data.
-        """
-        self.addFiles("others")
-
-    def addSourceDir(self):
-        """
-        Public slot to add all source files of a directory to the current
-        project.
-        """
-        self.addDirectory("source")
-
-    def addUiDir(self):
-        """
-        Public slot to add all forms of a directory to the current project.
-        """
-        self.addDirectory("form")
-
-    def addIdlDir(self):
-        """
-        Public slot to add all IDL interfaces of a directory to the current
-        project.
-        """
-        self.addDirectory("interface")
-
-    def addProtoDir(self):
-        """
-        Public slot to add all protocol files of a directory to the current
-        project.
-        """
-        self.addDirectory("protocol")
-
-    def addResourceDir(self):
-        """
-        Public slot to add all Qt resource files of a directory to the current
-        project.
-        """
-        self.addDirectory("resource")
-
-    def addOthersDir(self):
-        """
-        Public slot to add a directory to the OTHERS project data.
-        """
-        self.addDirectory("others")
-
     def renameMainScript(self, oldfn, newfn):
         """
         Public method to rename the main script.
@@ -2049,13 +2091,13 @@
         @param oldfn old filename (string)
         @param newfn new filename of the main script (string)
         """
-        if self.pdata["MAINSCRIPT"]:
+        if self.__pdata["MAINSCRIPT"]:
             ofn = self.getRelativePath(oldfn)
-            if ofn != self.pdata["MAINSCRIPT"]:
+            if ofn != self.__pdata["MAINSCRIPT"]:
                 return
 
             fn = self.getRelativePath(newfn)
-            self.pdata["MAINSCRIPT"] = fn
+            self.__pdata["MAINSCRIPT"] = fn
             self.setDirty(True)
 
     def renameFile(self, oldfn, newfn=None):
@@ -2067,7 +2109,7 @@
         @return flag indicating success
         """
         fn = self.getRelativePath(oldfn)
-        isSourceFile = fn in self.pdata["SOURCES"]
+        isSourceFile = fn in self.__pdata["SOURCES"]
 
         if newfn is None:
             newfn = EricFileDialog.getSaveFileName(
@@ -2107,22 +2149,14 @@
             )
             return False
 
-        if (
-            fn in self.pdata["SOURCES"]
-            or fn in self.pdata["FORMS"]
-            or fn in self.pdata["TRANSLATIONS"]
-            or fn in self.pdata["INTERFACES"]
-            or fn in self.pdata["PROTOCOLS"]
-            or fn in self.pdata["RESOURCES"]
-            or fn in self.pdata["OTHERS"]
-        ):
+        if any(fn in self.__pdata[category] for category in self.getFileCategories()):
             self.renameFileInPdata(oldfn, newfn, isSourceFile)
 
         return True
 
     def renameFileInPdata(self, oldname, newname, isSourceFile=False):
         """
-        Public method to rename a file in the pdata structure.
+        Public method to rename a file in the __pdata structure.
 
         @param oldname old filename (string)
         @param newname new filename (string)
@@ -2151,15 +2185,10 @@
         """
         filelist = []
         start = self.getRelativePath(start)
-        for key in [
-            "SOURCES",
-            "FORMS",
-            "INTERFACES",
-            "PROTOCOLS",
-            "RESOURCES",
-            "OTHERS",
+        for fileCategory in [
+            c for c in self.getFileCategories() if c != "TRANSLATIONS"
         ]:
-            for entry in self.pdata[key][:]:
+            for entry in self.__pdata[fileCategory][:]:
                 if entry.startswith(start):
                     filelist.append(os.path.join(self.ppath, entry))
         return filelist
@@ -2172,52 +2201,37 @@
 
         # init data store for the reorganization
         newPdata = {}
-        for key in [
-            "SOURCES",
-            "FORMS",
-            "INTERFACES",
-            "PROTOCOLS",
-            "RESOURCES",
-            "OTHERS",
-            "TRANSLATIONS",
-        ]:
-            newPdata[key] = []
+        for fileCategory in self.getFileCategories():
+            newPdata[fileCategory] = []
 
         # iterate over all files checking for a reassignment
-        for key in [
-            "SOURCES",
-            "FORMS",
-            "INTERFACES",
-            "PROTOCOLS",
-            "RESOURCES",
-            "OTHERS",
-            "TRANSLATIONS",
-        ]:
-            for fn in self.pdata[key][:]:
-                filetype = key
+        for fileCategory in self.getFileCategories():
+            for fn in self.__pdata[fileCategory][:]:
+                filetype = fileCategory
                 bfn = os.path.basename(fn)
-                for pattern in sorted(self.pdata["FILETYPES"].keys(), reverse=True):
+                for pattern in sorted(self.__pdata["FILETYPES"].keys(), reverse=True):
                     if fnmatch.fnmatch(bfn, pattern):
-                        filetype = self.pdata["FILETYPES"][pattern]
+                        filetype = self.__pdata["FILETYPES"][pattern]
                         break
 
                 if filetype != "__IGNORE__":
                     newPdata[filetype].append(fn)
-                    if filetype != key:
+                    if filetype != fileCategory:
                         reorganized = True
 
         if reorganized:
             # copy the reorganized files back to the project
-            for key in [
-                "SOURCES",
-                "FORMS",
-                "INTERFACES",
-                "PROTOCOLS",
-                "RESOURCES",
-                "OTHERS",
-                "TRANSLATIONS",
-            ]:
-                self.pdata[key] = newPdata[key][:]
+            ##for key in [
+            ##"SOURCES",
+            ##"FORMS",
+            ##"INTERFACES",
+            ##"PROTOCOLS",
+            ##"RESOURCES",
+            ##"OTHERS",
+            ##"TRANSLATIONS",
+            ##]:
+            for fileCategory in self.getFileCategories():
+                self.__pdata[fileCategory] = newPdata[fileCategory][:]
 
             # repopulate the model
             self.__model.projectClosed(False)
@@ -2232,18 +2246,23 @@
         """
         olddn = self.getRelativePath(olddn)
         newdn = self.getRelativePath(newdn)
-        for key in [
-            "SOURCES",
-            "FORMS",
-            "INTERFACES",
-            "PROTOCOLS",
-            "RESOURCES",
-            "OTHERS",
+        ##for key in [
+        ##"SOURCES",
+        ##"FORMS",
+        ##"INTERFACES",
+        ##"PROTOCOLS",
+        ##"RESOURCES",
+        ##"OTHERS",
+        ##]:
+        for fileCategory in [
+            c for c in self.getFileCategories() if c != "TRANSLATIONS"
         ]:
-            for entry in self.pdata[key][:]:
+            for entry in self.__pdata[fileCategory][:]:
                 if entry.startswith(olddn):
                     entry = entry.replace(olddn, newdn)
-                    self.appendFile(os.path.join(self.ppath, entry), key == "SOURCES")
+                    self.appendFile(
+                        os.path.join(self.ppath, entry), fileCategory == "SOURCES"
+                    )
         self.setDirty(True)
 
     def moveDirectory(self, olddn, newdn):
@@ -2256,22 +2275,25 @@
         olddn = self.getRelativePath(olddn)
         newdn = self.getRelativePath(newdn)
         typeStrings = []
-        for key in [
-            "SOURCES",
-            "FORMS",
-            "INTERFACES",
-            "PROTOCOLS",
-            "RESOURCES",
-            "OTHERS",
+        ##for key in [
+        ##"SOURCES",
+        ##"FORMS",
+        ##"INTERFACES",
+        ##"PROTOCOLS",
+        ##"RESOURCES",
+        ##"OTHERS",
+        ##]:
+        for fileCategory in [
+            c for c in self.getFileCategories() if c != "TRANSLATIONS"
         ]:
-            for entry in self.pdata[key][:]:
+            for entry in self.__pdata[fileCategory][:]:
                 if entry.startswith(olddn):
-                    if key not in typeStrings:
-                        typeStrings.append(key)
-                    self.pdata[key].remove(entry)
+                    if fileCategory not in typeStrings:
+                        typeStrings.append(fileCategory)
+                    self.__pdata[fileCategory].remove(entry)
                     entry = entry.replace(olddn, newdn)
-                    self.pdata[key].append(entry)
-            if key == "OTHERS":
+                    self.__pdata[fileCategory].append(entry)
+            if fileCategory == "OTHERS":
                 if newdn not in self.otherssubdirs:
                     self.otherssubdirs.append(newdn)
             else:
@@ -2299,33 +2321,14 @@
             requested (boolean)
         """
         fn = self.getRelativePath(fn)
-        dirty = True
-        if fn in self.pdata["SOURCES"]:
-            self.pdata["SOURCES"].remove(fn)
-            self.projectSourceRemoved.emit(fn)
-        elif fn in self.pdata["FORMS"]:
-            self.pdata["FORMS"].remove(fn)
-            self.projectFormRemoved.emit(fn)
-        elif fn in self.pdata["INTERFACES"]:
-            self.pdata["INTERFACES"].remove(fn)
-            self.projectInterfaceRemoved.emit(fn)
-        elif fn in self.pdata["PROTOCOLS"]:
-            self.pdata["PROTOCOLS"].remove(fn)
-            self.projectProtocolRemoved.emit(fn)
-        elif fn in self.pdata["RESOURCES"]:
-            self.pdata["RESOURCES"].remove(fn)
-            self.projectResourceRemoved.emit(fn)
-        elif fn in self.pdata["OTHERS"]:
-            self.pdata["OTHERS"].remove(fn)
-            self.projectOthersRemoved.emit(fn)
-        elif fn in self.pdata["TRANSLATIONS"]:
-            self.pdata["TRANSLATIONS"].remove(fn)
-            self.projectLanguageRemoved.emit(fn)
-        else:
-            dirty = False
-        updateModel and self.__model.removeItem(fn)
-        if dirty:
-            self.setDirty(True)
+        for fileCategory in self.getFileCategories():
+            if fn in self.__pdata[fileCategory]:
+                self.__pdata[fileCategory].remove(fn)
+                self.projectFileRemoved.emit(fn, fileCategory)
+                self.setDirty(True)
+                if updateModel:
+                    self.__model.removeItem(fn)
+                break
 
     def removeDirectory(self, dn):
         """
@@ -2337,22 +2340,15 @@
         """
         dirty = False
         dn = self.getRelativePath(dn)
-        for entry in self.pdata["OTHERS"][:]:
+        for entry in self.__pdata["OTHERS"][:]:
             if entry.startswith(dn):
-                self.pdata["OTHERS"].remove(entry)
+                self.__pdata["OTHERS"].remove(entry)
                 dirty = True
         dn2 = dn if dn.endswith(os.sep) else dn + os.sep
-        for key in [
-            "SOURCES",
-            "FORMS",
-            "INTERFACES",
-            "PROTOCOLS",
-            "RESOURCES",
-            "TRANSLATIONS",
-        ]:
-            for entry in self.pdata[key][:]:
+        for fileCategory in [c for c in self.getFileCategories() if c != "OTHERS"]:
+            for entry in self.__pdata[fileCategory][:]:
                 if entry.startswith(dn2):
-                    self.pdata[key].remove(entry)
+                    self.__pdata[fileCategory].remove(entry)
                     dirty = True
         self.__model.removeItem(dn)
         if dirty:
@@ -2367,7 +2363,7 @@
         @return flag indicating success (boolean)
         """
         try:
-            from send2trash import send2trash as s2t
+            from send2trash import send2trash as s2t  # __IGNORE_WARNING_I10__
         except ImportError:
             s2t = os.remove
 
@@ -2415,7 +2411,7 @@
             dn = os.path.join(self.ppath, dn)
         try:
             try:
-                from send2trash import send2trash
+                from send2trash import send2trash  # __IGNORE_WARNING_I10__
 
                 send2trash(dn)
             except ImportError:
@@ -2442,13 +2438,10 @@
         @return flag indicating, if the project contains the file (boolean)
         """
         fn = self.getRelativePath(fn)
-        return (
-            fn in self.pdata["SOURCES"]
-            or fn in self.pdata["FORMS"]
-            or fn in self.pdata["INTERFACES"]
-            or fn in self.pdata["PROTOCOLS"]
-            or fn in self.pdata["RESOURCES"]
-            or fn in self.pdata["OTHERS"]
+        return any(
+            fn in self.__pdata[category]
+            for category in self.getFileCategories()
+            if category != "TRANSLATIONS"
         )
 
     def createNewProject(self):
@@ -2458,20 +2451,23 @@
         This method displays the new project dialog and initializes
         the project object with the data entered.
         """
+        from eric7.VCS.CommandOptionsDialog import VcsCommandOptionsDialog
+
+        from .PropertiesDialog import PropertiesDialog
+
         if not self.checkDirty():
             return
 
-        from .PropertiesDialog import PropertiesDialog
-
         dlg = PropertiesDialog(self, True)
         if dlg.exec() == QDialog.DialogCode.Accepted:
             self.closeProject()
             dlg.storeData()
-            self.pdata["VCS"] = "None"
+            self.__pdata["VCS"] = "None"
             self.opened = True
-            if not self.pdata["FILETYPES"]:
+            if not self.__pdata["FILETYPES"]:
                 self.initFileTypes()
             self.setDirty(True)
+            self.reloadAct.setEnabled(True)
             self.closeAct.setEnabled(True)
             self.saveasAct.setEnabled(True)
             self.actGrp2.setEnabled(True)
@@ -2488,10 +2484,10 @@
             self.menuDiagramAct.setEnabled(True)
             self.menuApidocAct.setEnabled(True)
             self.menuPackagersAct.setEnabled(True)
-            self.pluginGrp.setEnabled(self.pdata["PROJECTTYPE"] in ["E7Plugin"])
-            self.addLanguageAct.setEnabled(bool(self.pdata["TRANSLATIONPATTERN"]))
-            self.makeGrp.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"])
-            self.menuMakeAct.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"])
+            self.pluginGrp.setEnabled(self.__pdata["PROJECTTYPE"] in ["E7Plugin"])
+            self.addLanguageAct.setEnabled(bool(self.__pdata["TRANSLATIONPATTERN"]))
+            self.makeGrp.setEnabled(self.__pdata["MAKEPARAMS"]["MakeEnabled"])
+            self.menuMakeAct.setEnabled(self.__pdata["MAKEPARAMS"]["MakeEnabled"])
             self.menuOtherToolsAct.setEnabled(True)
             self.menuFormattingAct.setEnabled(True)
 
@@ -2504,11 +2500,11 @@
                 ).toHex(),
                 encoding="utf-8",
             )
-            self.pdata["HASH"] = hashStr
-
-            if self.pdata["PROGLANGUAGE"] == "MicroPython":
+            self.__pdata["HASH"] = hashStr
+
+            if self.__pdata["PROGLANGUAGE"] == "MicroPython":
                 # change the lexer association for *.py files
-                self.pdata["LEXERASSOCS"] = {
+                self.__pdata["LEXERASSOCS"] = {
                     "*.py": "MicroPython",
                 }
 
@@ -2530,25 +2526,25 @@
 
                 # create an empty __init__.py file to make it a Python package
                 # (only for Python and Python3)
-                if self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]:
+                if self.__pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]:
                     fn = os.path.join(self.ppath, "__init__.py")
                     with open(fn, "w", encoding="utf-8"):
                         pass
                     self.appendFile(fn, True)
 
                 # create an empty main script file, if a name was given
-                if self.pdata["MAINSCRIPT"]:
-                    if not os.path.isabs(self.pdata["MAINSCRIPT"]):
-                        ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"])
+                if self.__pdata["MAINSCRIPT"]:
+                    if not os.path.isabs(self.__pdata["MAINSCRIPT"]):
+                        ms = os.path.join(self.ppath, self.__pdata["MAINSCRIPT"])
                     else:
-                        ms = self.pdata["MAINSCRIPT"]
+                        ms = self.__pdata["MAINSCRIPT"]
                     os.makedirs(os.path.dirname(ms), exist_ok=True)
                     with open(ms, "w"):
                         pass
                     self.appendFile(ms, True)
 
-                if self.pdata["MAKEPARAMS"]["MakeEnabled"]:
-                    mf = self.pdata["MAKEPARAMS"]["MakeFile"]
+                if self.__pdata["MAKEPARAMS"]["MakeEnabled"]:
+                    mf = self.__pdata["MAKEPARAMS"]["MakeFile"]
                     if mf:
                         if not os.path.isabs(mf):
                             mf = os.path.join(self.ppath, mf)
@@ -2564,8 +2560,8 @@
                     tpd = os.path.dirname(tpd)
                 if not os.path.isdir(tpd):
                     os.makedirs(tpd, exist_ok=True)
-                if self.pdata["TRANSLATIONSBINPATH"]:
-                    tpd = os.path.join(self.ppath, self.pdata["TRANSLATIONSBINPATH"])
+                if self.__pdata["TRANSLATIONSBINPATH"]:
+                    tpd = os.path.join(self.ppath, self.__pdata["TRANSLATIONSBINPATH"])
                     if not os.path.isdir(tpd):
                         os.makedirs(tpd, exist_ok=True)
 
@@ -2589,11 +2585,11 @@
                     )
                     return
 
-                if self.pdata["MAINSCRIPT"]:
-                    if not os.path.isabs(self.pdata["MAINSCRIPT"]):
-                        ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"])
+                if self.__pdata["MAINSCRIPT"]:
+                    if not os.path.isabs(self.__pdata["MAINSCRIPT"]):
+                        ms = os.path.join(self.ppath, self.__pdata["MAINSCRIPT"])
                     else:
-                        ms = self.pdata["MAINSCRIPT"]
+                        ms = self.__pdata["MAINSCRIPT"]
                     if not os.path.exists(ms):
                         try:
                             os.makedirs(os.path.dirname(ms))
@@ -2612,8 +2608,8 @@
                 else:
                     ms = ""
 
-                if self.pdata["MAKEPARAMS"]["MakeEnabled"]:
-                    mf = self.pdata["MAKEPARAMS"]["MakeFile"]
+                if self.__pdata["MAKEPARAMS"]["MakeEnabled"]:
+                    mf = self.__pdata["MAKEPARAMS"]["MakeFile"]
                     if mf:
                         if not os.path.isabs(mf):
                             mf = os.path.join(self.ppath, mf)
@@ -2647,7 +2643,7 @@
                 addAllToVcs = res
                 # create an empty __init__.py file to make it a Python package
                 # if none exists (only for Python and Python3)
-                if self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]:
+                if self.__pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]:
                     fn = os.path.join(self.ppath, "__init__.py")
                     if not os.path.exists(fn):
                         with open(fn, "w", encoding="utf-8"):
@@ -2685,7 +2681,7 @@
                                 vcsSystem = "None"
                         else:
                             vcsSystem = vcsData[0][1]
-                        self.pdata["VCS"] = vcsSystem
+                        self.__pdata["VCS"] = vcsSystem
                         self.vcs = self.initVCS()
                         self.setDirty(True)
                         if self.vcs is not None:
@@ -2702,10 +2698,6 @@
                             else:
                                 vcores = False
                             if vcores:
-                                from eric7.VCS.CommandOptionsDialog import (
-                                    VcsCommandOptionsDialog,
-                                )
-
                                 codlg = VcsCommandOptionsDialog(self.vcs)
                                 if codlg.exec() == QDialog.DialogCode.Accepted:
                                     self.vcs.vcsSetOptions(codlg.getOptions())
@@ -2724,7 +2716,7 @@
                                     self.saveProject()
                                     self.vcs.vcsAdd(self.pfile)
                         else:
-                            self.pdata["VCS"] = "None"
+                            self.__pdata["VCS"] = "None"
                         self.saveProject()
                         break
 
@@ -2750,19 +2742,19 @@
                 if ok and vcsSelected != self.tr("None"):
                     for vcsSystem, vcsSystemDisplay in vcsSystemsDict.items():
                         if vcsSystemDisplay == vcsSelected:
-                            self.pdata["VCS"] = vcsSystem
+                            self.__pdata["VCS"] = vcsSystem
                             break
                     else:
-                        self.pdata["VCS"] = "None"
+                        self.__pdata["VCS"] = "None"
                 else:
-                    self.pdata["VCS"] = "None"
+                    self.__pdata["VCS"] = "None"
                 self.vcs = self.initVCS()
                 if self.vcs is not None:
                     vcsdlg = self.vcs.vcsOptionsDialog(self, self.name)
                     if vcsdlg.exec() == QDialog.DialogCode.Accepted:
                         vcsDataDict = vcsdlg.getData()
                     else:
-                        self.pdata["VCS"] = "None"
+                        self.__pdata["VCS"] = "None"
                         self.vcs = self.initVCS()
                 self.setDirty(True)
                 if self.vcs is not None:
@@ -2779,10 +2771,6 @@
                     else:
                         vcores = False
                     if vcores:
-                        from eric7.VCS.CommandOptionsDialog import (
-                            VcsCommandOptionsDialog,
-                        )
-
                         codlg = VcsCommandOptionsDialog(self.vcs)
                         if codlg.exec() == QDialog.DialogCode.Accepted:
                             self.vcs.vcsSetOptions(codlg.getOptions())
@@ -2799,9 +2787,9 @@
                 self.newProjectHooks.emit()
                 self.newProject.emit()
 
-            if self.pdata["EMBEDDED_VENV"]:
+            if self.__pdata["EMBEDDED_VENV"]:
                 self.__createEmbeddedEnvironment()
-            self.menuEnvironmentAct.setEnabled(self.pdata["EMBEDDED_VENV"])
+            self.menuEnvironmentAct.setEnabled(self.__pdata["EMBEDDED_VENV"])
 
     def newProjectAddFiles(self, mainscript):
         """
@@ -2814,7 +2802,7 @@
 
         with EricOverrideCursor():
             # search the project directory for files with known extensions
-            filespecs = list(self.pdata["FILETYPES"].keys())
+            filespecs = list(self.__pdata["FILETYPES"].keys())
             for filespec in filespecs:
                 files = Utilities.direntries(self.ppath, True, filespec)
                 for file in files:
@@ -2828,12 +2816,12 @@
             else:
                 tpd = self.ppath
             tslist = []
-            if self.pdata["TRANSLATIONPATTERN"]:
-                pattern = os.path.basename(self.pdata["TRANSLATIONPATTERN"])
+            if self.__pdata["TRANSLATIONPATTERN"]:
+                pattern = os.path.basename(self.__pdata["TRANSLATIONPATTERN"])
                 if "%language%" in pattern:
                     pattern = pattern.replace("%language%", "*")
                 else:
-                    tpd = self.pdata["TRANSLATIONPATTERN"].split("%language%")[0]
+                    tpd = self.__pdata["TRANSLATIONPATTERN"].split("%language%")[0]
             else:
                 pattern = "*.ts"
             tslist.extend(Utilities.direntries(tpd, True, pattern))
@@ -2847,7 +2835,7 @@
                         os.path.splitext(mainscript)[0]
                         or os.path.basename(tslist[0]).split("_")[0]
                     )
-                    self.pdata["TRANSLATIONPATTERN"] = os.path.join(
+                    self.__pdata["TRANSLATIONPATTERN"] = os.path.join(
                         os.path.dirname(tslist[0]),
                         "{0}_%language%{1}".format(
                             os.path.basename(tslist[0]).split("_")[0],
@@ -2868,35 +2856,35 @@
                         tslist[0],
                     )
                     if pattern:
-                        self.pdata["TRANSLATIONPATTERN"] = pattern
-                if self.pdata["TRANSLATIONPATTERN"]:
-                    self.pdata["TRANSLATIONPATTERN"] = self.getRelativePath(
-                        self.pdata["TRANSLATIONPATTERN"]
+                        self.__pdata["TRANSLATIONPATTERN"] = pattern
+                if self.__pdata["TRANSLATIONPATTERN"]:
+                    self.__pdata["TRANSLATIONPATTERN"] = self.getRelativePath(
+                        self.__pdata["TRANSLATIONPATTERN"]
                     )
-                    pattern = self.pdata["TRANSLATIONPATTERN"].replace(
+                    pattern = self.__pdata["TRANSLATIONPATTERN"].replace(
                         "%language%", "*"
                     )
                     for ts in tslist:
                         if fnmatch.fnmatch(ts, pattern):
-                            self.pdata["TRANSLATIONS"].append(ts)
-                            self.projectLanguageAdded.emit(ts)
-                    if self.pdata["TRANSLATIONSBINPATH"]:
+                            self.__pdata["TRANSLATIONS"].append(ts)
+                            self.projectFileAdded.emit(ts, "TRANSLATIONS")
+                    if self.__pdata["TRANSLATIONSBINPATH"]:
                         tpd = os.path.join(
-                            self.ppath, self.pdata["TRANSLATIONSBINPATH"]
+                            self.ppath, self.__pdata["TRANSLATIONSBINPATH"]
                         )
                         pattern = os.path.basename(
-                            self.pdata["TRANSLATIONPATTERN"]
+                            self.__pdata["TRANSLATIONPATTERN"]
                         ).replace("%language%", "*")
                         pattern = self.__binaryTranslationFile(pattern)
                         qmlist = Utilities.direntries(tpd, True, pattern)
                         for qm in qmlist:
-                            self.pdata["TRANSLATIONS"].append(qm)
-                            self.projectLanguageAdded.emit(qm)
-                if not self.pdata["MAINSCRIPT"] and bool(mainscriptname):
-                    if self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]:
-                        self.pdata["MAINSCRIPT"] = "{0}.py".format(mainscriptname)
-                    elif self.pdata["PROGLANGUAGE"] == "Ruby":
-                        self.pdata["MAINSCRIPT"] = "{0}.rb".format(mainscriptname)
+                            self.__pdata["TRANSLATIONS"].append(qm)
+                            self.projectFileAdded.emit(qm, "TRANSLATIONS")
+                if not self.__pdata["MAINSCRIPT"] and bool(mainscriptname):
+                    if self.__pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]:
+                        self.__pdata["MAINSCRIPT"] = "{0}.py".format(mainscriptname)
+                    elif self.__pdata["PROGLANGUAGE"] == "Ruby":
+                        self.__pdata["MAINSCRIPT"] = "{0}.rb".format(mainscriptname)
             self.setDirty(True)
 
     def __showProperties(self):
@@ -2907,19 +2895,19 @@
 
         dlg = PropertiesDialog(self, False)
         if dlg.exec() == QDialog.DialogCode.Accepted:
-            projectType = self.pdata["PROJECTTYPE"]
+            projectType = self.__pdata["PROJECTTYPE"]
             dlg.storeData()
             self.setDirty(True)
-            if self.pdata["MAINSCRIPT"]:
-                if not os.path.isabs(self.pdata["MAINSCRIPT"]):
-                    ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"])
+            if self.__pdata["MAINSCRIPT"]:
+                if not os.path.isabs(self.__pdata["MAINSCRIPT"]):
+                    ms = os.path.join(self.ppath, self.__pdata["MAINSCRIPT"])
                 else:
-                    ms = self.pdata["MAINSCRIPT"]
+                    ms = self.__pdata["MAINSCRIPT"]
                 if os.path.exists(ms):
                     self.appendFile(ms)
 
-            if self.pdata["MAKEPARAMS"]["MakeEnabled"]:
-                mf = self.pdata["MAKEPARAMS"]["MakeFile"]
+            if self.__pdata["MAKEPARAMS"]["MakeEnabled"]:
+                mf = self.__pdata["MAKEPARAMS"]["MakeFile"]
                 if mf:
                     if not os.path.isabs(mf):
                         mf = os.path.join(self.ppath, mf)
@@ -2940,7 +2928,7 @@
                         )
                 self.appendFile(mf)
 
-            if self.pdata["PROJECTTYPE"] != projectType:
+            if self.__pdata["PROJECTTYPE"] != projectType:
                 # reinitialize filetype associations
                 self.initFileTypes()
 
@@ -2955,39 +2943,39 @@
             if tp != self.ppath and tp not in self.subdirs:
                 self.subdirs.append(tp)
 
-            if self.pdata["TRANSLATIONSBINPATH"]:
-                tp = os.path.join(self.ppath, self.pdata["TRANSLATIONSBINPATH"])
+            if self.__pdata["TRANSLATIONSBINPATH"]:
+                tp = os.path.join(self.ppath, self.__pdata["TRANSLATIONSBINPATH"])
                 if not os.path.isdir(tp):
                     os.makedirs(tp)
                 if tp != self.ppath and tp not in self.subdirs:
                     self.subdirs.append(tp)
 
-            self.pluginGrp.setEnabled(self.pdata["PROJECTTYPE"] in ["E7Plugin"])
+            self.pluginGrp.setEnabled(self.__pdata["PROJECTTYPE"] in ["E7Plugin"])
 
             self.__model.projectPropertiesChanged()
             self.projectPropertiesChanged.emit()
 
-            if self.pdata["PROJECTTYPE"] != projectType:
+            if self.__pdata["PROJECTTYPE"] != projectType:
                 self.__reorganizeFiles()
 
-            if self.pdata["EMBEDDED_VENV"] and not self.__findEmbeddedEnvironment():
+            if self.__pdata["EMBEDDED_VENV"] and not self.__findEmbeddedEnvironment():
                 self.__createEmbeddedEnvironment()
 
     def __showUserProperties(self):
         """
         Private slot to display the user specific properties dialog.
         """
-        vcsSystem = self.pdata["VCS"] or None
+        from .UserPropertiesDialog import UserPropertiesDialog
+
+        vcsSystem = self.__pdata["VCS"] or None
         vcsSystemOverride = self.pudata["VCSOVERRIDE"] or None
 
-        from .UserPropertiesDialog import UserPropertiesDialog
-
         dlg = UserPropertiesDialog(self)
         if dlg.exec() == QDialog.DialogCode.Accepted:
             dlg.storeData()
 
             if (
-                (self.pdata["VCS"] and self.pdata["VCS"] != vcsSystem)
+                (self.__pdata["VCS"] and self.__pdata["VCS"] != vcsSystem)
                 or (
                     self.pudata["VCSOVERRIDE"]
                     and self.pudata["VCSOVERRIDE"] != vcsSystemOverride
@@ -3031,17 +3019,16 @@
         Public method to get the list of file type associations for
         the given association type.
 
-        @param associationType type of the association (one of FORMS,
-            INTERFACES, OTHERS, PROTOCOLS, RESOURCES, SOURCES,
-            TRANSLATIONS or __IGNORE__)
+        @param associationType type of the association (one of the known file categories
+            or __IGNORE__)
         @type str
         @return list of file patterns for the given type
         @rtype list of str
         """
         return [
             assoc
-            for assoc in self.pdata["FILETYPES"]
-            if self.pdata["FILETYPES"][assoc] == associationType
+            for assoc in self.__pdata["FILETYPES"]
+            if self.__pdata["FILETYPES"][assoc] == associationType
         ]
 
     def __showLexerAssociations(self):
@@ -3065,12 +3052,12 @@
         @return the requested lexer language (string)
         """
         # try user settings first
-        for pattern, language in list(self.pdata["LEXERASSOCS"].items()):
+        for pattern, language in list(self.__pdata["LEXERASSOCS"].items()):
             if fnmatch.fnmatch(filename, pattern):
                 return language
 
         # try project type specific defaults next
-        projectType = self.pdata["PROJECTTYPE"]
+        projectType = self.__pdata["PROJECTTYPE"]
         with contextlib.suppress(KeyError):
             if self.__lexerAssociationCallbacks[projectType] is not None:
                 return self.__lexerAssociationCallbacks[projectType](filename)
@@ -3115,7 +3102,7 @@
                 ok = self.__readProject(fn)
             if ok:
                 self.opened = True
-                if not self.pdata["FILETYPES"]:
+                if not self.__pdata["FILETYPES"]:
                     self.initFileTypes()
                 else:
                     self.updateFileTypes()
@@ -3172,15 +3159,16 @@
                                         vcsSystem = "None"
                                 else:
                                     vcsSystem = vcsData[0][0]
-                                self.pdata["VCS"] = vcsSystem
+                                self.__pdata["VCS"] = vcsSystem
                                 self.vcs = self.initVCS()
                                 self.setDirty(True)
                     if self.vcs is not None and (
                         self.vcs.vcsRegisteredState(self.ppath)
                         != self.vcs.canBeCommitted
                     ):
-                        self.pdata["VCS"] = "None"
+                        self.__pdata["VCS"] = "None"
                         self.vcs = self.initVCS()
+                    self.reloadAct.setEnabled(True)
                     self.closeAct.setEnabled(True)
                     self.saveasAct.setEnabled(True)
                     self.actGrp2.setEnabled(True)
@@ -3197,12 +3185,16 @@
                     self.menuDiagramAct.setEnabled(True)
                     self.menuApidocAct.setEnabled(True)
                     self.menuPackagersAct.setEnabled(True)
-                    self.pluginGrp.setEnabled(self.pdata["PROJECTTYPE"] in ["E7Plugin"])
+                    self.pluginGrp.setEnabled(
+                        self.__pdata["PROJECTTYPE"] in ["E7Plugin"]
+                    )
                     self.addLanguageAct.setEnabled(
-                        bool(self.pdata["TRANSLATIONPATTERN"])
+                        bool(self.__pdata["TRANSLATIONPATTERN"])
                     )
-                    self.makeGrp.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"])
-                    self.menuMakeAct.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"])
+                    self.makeGrp.setEnabled(self.__pdata["MAKEPARAMS"]["MakeEnabled"])
+                    self.menuMakeAct.setEnabled(
+                        self.__pdata["MAKEPARAMS"]["MakeEnabled"]
+                    )
                     self.menuOtherToolsAct.setEnabled(True)
                     self.menuFormattingAct.setEnabled(True)
 
@@ -3213,7 +3205,7 @@
 
                     self.__model.projectOpened()
 
-                if self.pdata["EMBEDDED_VENV"]:
+                if self.__pdata["EMBEDDED_VENV"]:
                     envPath = self.__findEmbeddedEnvironment()
                     if bool(envPath):
                         self.__loadEnvironmentConfiguration()
@@ -3225,7 +3217,7 @@
                             self.__configureEnvironment(envPath)
                     else:
                         self.__createEmbeddedEnvironment()
-                self.menuEnvironmentAct.setEnabled(self.pdata["EMBEDDED_VENV"])
+                self.menuEnvironmentAct.setEnabled(self.__pdata["EMBEDDED_VENV"])
 
                 self.projectOpenedHooks.emit()
                 self.projectOpened.emit()
@@ -3242,11 +3234,11 @@
 
                 if restoreSession:
                     # open the main script
-                    if self.pdata["MAINSCRIPT"]:
-                        if not os.path.isabs(self.pdata["MAINSCRIPT"]):
-                            ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"])
+                    if self.__pdata["MAINSCRIPT"]:
+                        if not os.path.isabs(self.__pdata["MAINSCRIPT"]):
+                            ms = os.path.join(self.ppath, self.__pdata["MAINSCRIPT"])
                         else:
-                            ms = self.pdata["MAINSCRIPT"]
+                            ms = self.__pdata["MAINSCRIPT"]
                         self.sourceFile.emit(ms)
 
                     # open a project session file being quiet about errors
@@ -3459,6 +3451,7 @@
         self.__closeAllWindows()
 
         self.__initData()
+        self.reloadAct.setEnabled(False)
         self.closeAct.setEnabled(False)
         self.saveasAct.setEnabled(False)
         self.saveAct.setEnabled(False)
@@ -3502,7 +3495,7 @@
         filesWithSyntaxErrors = 0
         for fn in vm.getOpenFilenames():
             rfn = self.getRelativePath(fn)
-            if rfn in self.pdata["SOURCES"] or rfn in self.pdata["OTHERS"]:
+            if rfn in self.__pdata["SOURCES"] or rfn in self.__pdata["OTHERS"]:
                 editor = vm.getOpenEditor(fn)
                 success &= vm.saveEditorEd(editor)
                 if reportSyntaxErrors and editor.hasSyntaxErrors():
@@ -3536,7 +3529,7 @@
         filesWithSyntaxErrors = 0
         for fn in vm.getOpenFilenames():
             rfn = self.getRelativePath(fn)
-            if rfn in self.pdata["SOURCES"] or rfn in self.pdata["OTHERS"]:
+            if rfn in self.__pdata["SOURCES"] or rfn in self.__pdata["OTHERS"]:
                 editor = vm.getOpenEditor(fn)
                 success &= editor.checkDirty()
                 if reportSyntaxErrors and editor.hasSyntaxErrors():
@@ -3568,11 +3561,11 @@
         @return filename of the projects main script
         @rtype str
         """
-        if self.pdata["MAINSCRIPT"]:
+        if self.__pdata["MAINSCRIPT"]:
             if normalized:
-                return os.path.join(self.ppath, self.pdata["MAINSCRIPT"])
+                return os.path.join(self.ppath, self.__pdata["MAINSCRIPT"])
             else:
-                return self.pdata["MAINSCRIPT"]
+                return self.__pdata["MAINSCRIPT"]
         else:
             return ""
 
@@ -3591,8 +3584,7 @@
         """
         Public method to get the file entries of the given type.
 
-        @param fileType project file type (one of SOURCES, FORMS, RESOURCES,
-            INTERFACES, PROTOCOLS, OTHERS, TRANSLATIONS)
+        @param fileType project file type (one of the known file categories)
         @type str
         @param normalized flag indicating normalized file names are wanted
         @type boolean
@@ -3600,21 +3592,13 @@
         @rtype list of str
         @exception ValueError raised when an unsupported file type is given
         """
-        if fileType not in [
-            "SOURCES",
-            "FORMS",
-            "RESOURCES",
-            "INTERFACES",
-            "PROTOCOLS",
-            "OTHERS",
-            "TRANSLATIONS",
-        ]:
+        if fileType not in self.getFileCategories():
             raise ValueError("Given file type has incorrect value.")
 
         if normalized:
-            return [os.path.join(self.ppath, fn) for fn in self.pdata[fileType]]
+            return [os.path.join(self.ppath, fn) for fn in self.__pdata[fileType]]
         else:
-            return self.pdata[fileType]
+            return self.__pdata[fileType]
 
     def getProjectType(self):
         """
@@ -3622,7 +3606,7 @@
 
         @return UI type of the project (string)
         """
-        return self.pdata["PROJECTTYPE"]
+        return self.__pdata["PROJECTTYPE"]
 
     def getProjectLanguage(self):
         """
@@ -3630,7 +3614,7 @@
 
         @return programming language (string)
         """
-        return self.pdata["PROGLANGUAGE"]
+        return self.__pdata["PROGLANGUAGE"]
 
     def isMixedLanguageProject(self):
         """
@@ -3639,7 +3623,7 @@
         @return flag indicating a mixed language project
         @rtype bool
         """
-        return self.pdata["MIXEDLANGUAGE"]
+        return self.__pdata["MIXEDLANGUAGE"]
 
     def isPythonProject(self):
         """
@@ -3648,7 +3632,7 @@
 
         @return flag indicating a Python project (boolean)
         """
-        return self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]
+        return self.__pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]
 
     def isPy3Project(self):
         """
@@ -3656,7 +3640,7 @@
 
         @return flag indicating a Python3 project (boolean)
         """
-        return self.pdata["PROGLANGUAGE"] == "Python3"
+        return self.__pdata["PROGLANGUAGE"] == "Python3"
 
     def isMicroPythonProject(self):
         """
@@ -3665,7 +3649,7 @@
         @return flag indicating a MicroPython project
         @rtype bool
         """
-        return self.pdata["PROGLANGUAGE"] == "MicroPython"
+        return self.__pdata["PROGLANGUAGE"] == "MicroPython"
 
     def isRubyProject(self):
         """
@@ -3673,7 +3657,7 @@
 
         @return flag indicating a Ruby project (boolean)
         """
-        return self.pdata["PROGLANGUAGE"] == "Ruby"
+        return self.__pdata["PROGLANGUAGE"] == "Ruby"
 
     def isJavaScriptProject(self):
         """
@@ -3681,7 +3665,7 @@
 
         @return flag indicating a JavaScript project (boolean)
         """
-        return self.pdata["PROGLANGUAGE"] == "JavaScript"
+        return self.__pdata["PROGLANGUAGE"] == "JavaScript"
 
     def getProjectSpellLanguage(self):
         """
@@ -3689,7 +3673,7 @@
 
         @return programming language (string)
         """
-        return self.pdata["SPELLLANGUAGE"]
+        return self.__pdata["SPELLLANGUAGE"]
 
     def getProjectDictionaries(self):
         """
@@ -3699,14 +3683,14 @@
             project specific word and exclude list
         """
         pwl = ""
-        if self.pdata["SPELLWORDS"]:
-            pwl = os.path.join(self.ppath, self.pdata["SPELLWORDS"])
+        if self.__pdata["SPELLWORDS"]:
+            pwl = os.path.join(self.ppath, self.__pdata["SPELLWORDS"])
             if not os.path.isfile(pwl):
                 pwl = ""
 
         pel = ""
-        if self.pdata["SPELLEXCLUDES"]:
-            pel = os.path.join(self.ppath, self.pdata["SPELLEXCLUDES"])
+        if self.__pdata["SPELLEXCLUDES"]:
+            pel = os.path.join(self.ppath, self.__pdata["SPELLEXCLUDES"])
             if not os.path.isfile(pel):
                 pel = ""
 
@@ -3719,7 +3703,7 @@
 
         @return default extension (including the dot) (string)
         """
-        lang = self.pdata["PROGLANGUAGE"]
+        lang = self.__pdata["PROGLANGUAGE"]
         if lang in ("", "Python"):
             lang = "Python3"
         return self.__sourceExtensions(lang)[0]
@@ -3795,7 +3779,7 @@
 
         @return project hash as a hex string (string)
         """
-        return self.pdata["HASH"]
+        return self.__pdata["HASH"]
 
     def getRelativePath(self, path):
         """
@@ -3852,8 +3836,8 @@
 
         @return eol string (string)
         """
-        if self.pdata["EOL"] >= 0:
-            return self.eols[self.pdata["EOL"]]
+        if self.__pdata["EOL"] >= 0:
+            return self.eols[self.__pdata["EOL"]]
         else:
             eolMode = Preferences.getEditor("EOLMode")
             if eolMode == QsciScintilla.EolMode.EolWindows:
@@ -3872,7 +3856,7 @@
 
         @return flag indicating the usage of system eol (boolean)
         """
-        return self.pdata["EOL"] == 0
+        return self.__pdata["EOL"] == 0
 
     def getProjectVersion(self):
         """
@@ -3881,7 +3865,7 @@
         @return version number
         @rtype str
         """
-        return self.pdata["VERSION"]
+        return self.__pdata["VERSION"]
 
     def getProjectAuthor(self):
         """
@@ -3890,7 +3874,7 @@
         @return author name
         @rtype str
         """
-        return self.pdata["AUTHOR"]
+        return self.__pdata["AUTHOR"]
 
     def getProjectAuthorEmail(self):
         """
@@ -3899,7 +3883,7 @@
         @return project author email
         @rtype str
         """
-        return self.pdata["EMAIL"]
+        return self.__pdata["EMAIL"]
 
     def getProjectDescription(self):
         """
@@ -3908,7 +3892,7 @@
         @return project description
         @rtype str
         """
-        return self.pdata["DESCRIPTION"]
+        return self.__pdata["DESCRIPTION"]
 
     def getProjectVenv(self, resolveDebugger=True):
         """
@@ -3923,7 +3907,7 @@
         """
         venvName = (
             self.__venvConfiguration["name"]
-            if self.pdata["EMBEDDED_VENV"] and bool(self.__venvConfiguration["name"])
+            if self.__pdata["EMBEDDED_VENV"] and bool(self.__venvConfiguration["name"])
             else self.getDebugProperty("VIRTUALENV")
         )
         if (
@@ -3948,7 +3932,7 @@
         """
         interpreter = (
             self.__venvConfiguration["interpreter"]
-            if self.pdata["EMBEDDED_VENV"]
+            if self.__pdata["EMBEDDED_VENV"]
             else ""
         )
         if not interpreter:
@@ -3971,7 +3955,7 @@
         @return executable search path prefix
         @rtype str
         """
-        if self.pdata["EMBEDDED_VENV"]:
+        if self.__pdata["EMBEDDED_VENV"]:
             execPath = self.__venvConfiguration["exec_path"]
         else:
             execPath = ""
@@ -3993,7 +3977,7 @@
         @rtype str
         """
         try:
-            return self.pdata["TESTING_FRAMEWORK"]
+            return self.__pdata["TESTING_FRAMEWORK"]
         except KeyError:
             return ""
 
@@ -4005,7 +3989,7 @@
         @rtype str
         """
         try:
-            return self.pdata["LICENSE"]
+            return self.__pdata["LICENSE"]
         except KeyError:
             return ""
 
@@ -4022,16 +4006,7 @@
         newfn = os.path.abspath(fn)
         newfn = self.getRelativePath(newfn)
         return any(
-            newfn in self.pdata[group]
-            for group in [
-                "SOURCES",
-                "FORMS",
-                "INTERFACES",
-                "PROTOCOLS",
-                "RESOURCES",
-                "TRANSLATIONS",
-                "OTHERS",
-            ]
+            newfn in self.__pdata[category] for category in self.getFileCategories()
         )
 
     def isProjectFile(self, fn):
@@ -4043,16 +4018,8 @@
         @return flag indicating membership (boolean)
         """
         return any(
-            self.__checkProjectFileGroup(fn, group)
-            for group in [
-                "SOURCES",
-                "FORMS",
-                "INTERFACES",
-                "PROTOCOLS",
-                "RESOURCES",
-                "TRANSLATIONS",
-                "OTHERS",
-            ]
+            self.__checkProjectFileGroup(fn, category)
+            for category in self.getFileCategories()
         )
 
     def __checkProjectFileGroup(self, fn, group):
@@ -4066,75 +4033,41 @@
         """
         newfn = os.path.abspath(fn)
         newfn = self.getRelativePath(newfn)
-        if newfn in self.pdata[group] or (
+        if newfn in self.__pdata[group] or (
             group == "OTHERS"
-            and any(newfn.startswith(entry) for entry in self.pdata[group])
+            and any(newfn.startswith(entry) for entry in self.__pdata[group])
         ):
             return True
 
         if Utilities.isWindowsPlatform():
             # try the above case-insensitive
             newfn = newfn.lower()
-            if any(entry.lower() == newfn for entry in self.pdata[group]):
+            if any(entry.lower() == newfn for entry in self.__pdata[group]):
                 return True
-        elif group == "OTHERS" and any(
-            newfn.startswith(entry.lower()) for entry in self.pdata[group]
-        ):
-            return True
+
+            elif group == "OTHERS" and any(
+                newfn.startswith(entry.lower()) for entry in self.__pdata[group]
+            ):
+                return True
 
         return False
 
-    def isProjectSource(self, fn):
-        """
-        Public method used to check, if the passed in filename belongs to the
-        project sources.
-
-        @param fn filename to be checked (string)
-        @return flag indicating membership (boolean)
-        """
-        return self.__checkProjectFileGroup(fn, "SOURCES")
-
-    def isProjectForm(self, fn):
-        """
-        Public method used to check, if the passed in filename belongs to the
-        project forms.
-
-        @param fn filename to be checked (string)
-        @return flag indicating membership (boolean)
-        """
-        return self.__checkProjectFileGroup(fn, "FORMS")
-
-    def isProjectInterface(self, fn):
-        """
-        Public method used to check, if the passed in filename belongs to the
-        project interfaces.
-
-        @param fn filename to be checked (string)
-        @return flag indicating membership (boolean)
-        """
-        return self.__checkProjectFileGroup(fn, "INTERFACES")
-
-    def isProjectProtocol(self, fn):
-        """
-        Public method used to check, if the passed in filename belongs to the
-        project protocols.
+    def isProjectCategory(self, fn, category):
+        """
+        Public method to check, if the passed in filename belongs to the given
+        category.
 
         @param fn filename to be checked
         @type str
+        @param category file category to check against
+        @type str
         @return flag indicating membership
         @rtype bool
         """
-        return self.__checkProjectFileGroup(fn, "PROTOCOLS")
-
-    def isProjectResource(self, fn):
-        """
-        Public method used to check, if the passed in filename belongs to the
-        project resources.
-
-        @param fn filename to be checked (string)
-        @return flag indicating membership (boolean)
-        """
-        return self.__checkProjectFileGroup(fn, "RESOURCES")
+        if category in self.getFileCategories():
+            return self.__checkProjectFileGroup(fn, category)
+        else:
+            return False  # unknown category always returns False
 
     def initActions(self):
         """
@@ -4179,11 +4112,27 @@
         )
         act.setStatusTip(self.tr("Open an existing project"))
         act.setWhatsThis(
-            self.tr("""<b>Open...</b>""" """<p>This opens an existing project.</p>""")
+            self.tr("""<b>Open...</b><p>This opens an existing project.</p>""")
         )
         act.triggered.connect(self.openProject)
         self.actions.append(act)
 
+        self.reloadAct = EricAction(
+            self.tr("Reload project"),
+            EricPixmapCache.getIcon("projectReload"),
+            self.tr("&Reload"),
+            0,
+            0,
+            self.actGrp1,
+            "project_reload",
+        )
+        self.reloadAct.setStatusTip(self.tr("Reload the current project"))
+        self.reloadAct.setWhatsThis(
+            self.tr("""<b>Reload</b><p>This reloads the current project.</p>""")
+        )
+        self.reloadAct.triggered.connect(self.reopenProject)
+        self.actions.append(self.reloadAct)
+
         self.closeAct = EricAction(
             self.tr("Close project"),
             EricPixmapCache.getIcon("projectClose"),
@@ -4195,7 +4144,7 @@
         )
         self.closeAct.setStatusTip(self.tr("Close the current project"))
         self.closeAct.setWhatsThis(
-            self.tr("""<b>Close</b>""" """<p>This closes the current project.</p>""")
+            self.tr("""<b>Close</b><p>This closes the current project.</p>""")
         )
         self.closeAct.triggered.connect(self.closeProject)
         self.actions.append(self.closeAct)
@@ -4211,7 +4160,7 @@
         )
         self.saveAct.setStatusTip(self.tr("Save the current project"))
         self.saveAct.setWhatsThis(
-            self.tr("""<b>Save</b>""" """<p>This saves the current project.</p>""")
+            self.tr("""<b>Save</b><p>This saves the current project.</p>""")
         )
         self.saveAct.triggered.connect(self.saveProject)
         self.actions.append(self.saveAct)
@@ -4867,7 +4816,7 @@
         self.actions.append(self.createSBOMAct)
 
         ###################################################################
-        ## Project Tools - code formatting actions
+        ## Project Tools - code formatting actions - Black
         ###################################################################
 
         self.blackFormattingGrp = createActionGroup(self)
@@ -4993,6 +4942,108 @@
         self.actions.append(self.blackConfigureAct)
 
         ###################################################################
+        ## Project Tools - code formatting actions - isort
+        ###################################################################
+
+        self.isortFormattingGrp = createActionGroup(self)
+
+        self.isortAboutAct = EricAction(
+            self.tr("About isort"),
+            self.tr("&isort"),
+            0,
+            0,
+            self.isortFormattingGrp,
+            "project_isort_about",
+        )
+        self.isortAboutAct.setStatusTip(self.tr("Show some information about 'isort'."))
+        self.isortAboutAct.setWhatsThis(
+            self.tr(
+                "<b>isort</b>"
+                "<p>This shows some information about the installed 'isort' tool.</p>"
+            )
+        )
+        self.isortAboutAct.triggered.connect(aboutIsort)
+        self.actions.append(self.isortAboutAct)
+        font = self.isortAboutAct.font()
+        font.setBold(True)
+        self.isortAboutAct.setFont(font)
+
+        self.isortSortImportsAct = EricAction(
+            self.tr("Sort Imports"),
+            self.tr("Sort Imports"),
+            0,
+            0,
+            self.isortFormattingGrp,
+            "project_isort_sort_imports",
+        )
+        self.isortSortImportsAct.setStatusTip(
+            self.tr("Sort the import statements of the project sources with 'isort'.")
+        )
+        self.isortSortImportsAct.setWhatsThis(
+            self.tr(
+                "<b>Sort Imports</b>"
+                "<p>This shows a dialog to enter parameters for the imports sorting"
+                " run and sorts the import statements of the project sources using"
+                " 'isort'.</p>"
+            )
+        )
+        self.isortSortImportsAct.triggered.connect(
+            lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Sort)
+        )
+        self.actions.append(self.isortSortImportsAct)
+
+        self.isortDiffSortingAct = EricAction(
+            self.tr("Imports Sorting Diff"),
+            self.tr("Imports Sorting Diff"),
+            0,
+            0,
+            self.isortFormattingGrp,
+            "project_isort_diff_code",
+        )
+        self.isortDiffSortingAct.setStatusTip(
+            self.tr(
+                "Generate a unified diff of potential project source imports"
+                " resorting with 'isort'."
+            )
+        )
+        self.isortDiffSortingAct.setWhatsThis(
+            self.tr(
+                "<b>Imports Sorting Diff</b>"
+                "<p>This shows a dialog to enter parameters for the imports sorting"
+                " diff run and generates a unified diff of potential project source"
+                " changes using 'isort'.</p>"
+            )
+        )
+        self.isortDiffSortingAct.triggered.connect(
+            lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Diff)
+        )
+        self.actions.append(self.isortDiffSortingAct)
+
+        self.isortConfigureAct = EricAction(
+            self.tr("Configure"),
+            self.tr("Configure"),
+            0,
+            0,
+            self.isortFormattingGrp,
+            "project_isort_configure",
+        )
+        self.isortConfigureAct.setStatusTip(
+            self.tr(
+                "Enter the parameters for resorting the project sources import"
+                " statements with 'isort'."
+            )
+        )
+        self.isortConfigureAct.setWhatsThis(
+            self.tr(
+                "<b>Configure</b>"
+                "<p>This shows a dialog to enter the parameters for resorting the"
+                " import statements of the project sources with 'isort'.</p>"
+            )
+        )
+        self.isortConfigureAct.triggered.connect(self.__configureIsort)
+        self.actions.append(self.isortConfigureAct)
+
+        ###################################################################
         ## Project - embedded environment actions
         ###################################################################
 
@@ -5081,6 +5132,7 @@
         self.recreateVenvAct.triggered.connect(self.__createEmbeddedEnvironment)
         self.actions.append(self.recreateVenvAct)
 
+        self.reloadAct.setEnabled(False)
         self.closeAct.setEnabled(False)
         self.saveAct.setEnabled(False)
         self.saveasAct.setEnabled(False)
@@ -5205,6 +5257,8 @@
         self.formattingMenu.setTearOffEnabled(True)
         self.formattingMenu.addActions(self.blackFormattingGrp.actions())
         self.formattingMenu.addSeparator()
+        self.formattingMenu.addActions(self.isortFormattingGrp.actions())
+        self.formattingMenu.addSeparator()
 
         # build the project main menu
         menu.setTearOffEnabled(True)
@@ -5275,6 +5329,8 @@
             (EricToolBarManager)
         @return tuple of the generated toolbars (tuple of two QToolBar)
         """
+        from eric7 import VCS
+
         tb = QToolBar(self.tr("Project"), self.ui)
         tb.setIconSize(Config.ToolBarIconSize)
         tb.setObjectName("ProjectToolbar")
@@ -5293,8 +5349,6 @@
         toolbarManager.addAction(self.propsAct, tb.windowTitle())
         toolbarManager.addAction(self.userPropsAct, tb.windowTitle())
 
-        from eric7 import VCS
-
         vcstb = VCS.getBasicHelper(self).initBasicToolbar(self.ui, toolbarManager)
 
         return tb, vcstb
@@ -5304,7 +5358,7 @@
         Private method to set up the project menu.
         """
         self.menuRecentAct.setEnabled(len(self.recent) > 0)
-        self.menuEnvironmentAct.setEnabled(self.pdata["EMBEDDED_VENV"])
+        self.menuEnvironmentAct.setEnabled(self.__pdata["EMBEDDED_VENV"])
 
         self.showMenu.emit("Main", self.__menus["Main"])
 
@@ -5381,9 +5435,9 @@
         """
         Private slot to show the Find Project File dialog.
         """
+        from .QuickFindFileDialog import QuickFindFileDialog
+
         if self.__findProjectFileDialog is None:
-            from .QuickFindFileDialog import QuickFindFileDialog
-
             self.__findProjectFileDialog = QuickFindFileDialog(self)
             self.__findProjectFileDialog.sourceFile.connect(self.sourceFile)
             self.__findProjectFileDialog.designerFile.connect(self.designerFile)
@@ -5406,13 +5460,15 @@
         @param onUserDemand flag indicating whether this method was
                 requested by the user via a menu action (boolean)
         """
+        from .AddFoundFilesDialog import AddFoundFilesDialog
+
         autoInclude = Preferences.getProject("AutoIncludeNewFiles")
         recursiveSearch = Preferences.getProject("SearchNewFilesRecursively")
         newFiles = []
 
         ignore_patterns = [
             pattern
-            for pattern, filetype in self.pdata["FILETYPES"].items()
+            for pattern, filetype in self.__pdata["FILETYPES"].items()
             if filetype == "__IGNORE__"
         ]
 
@@ -5432,8 +5488,8 @@
             except OSError:
                 newSources = []
             pattern = (
-                self.pdata["TRANSLATIONPATTERN"].replace("%language%", "*")
-                if self.pdata["TRANSLATIONPATTERN"]
+                self.__pdata["TRANSLATIONPATTERN"].replace("%language%", "*")
+                if self.__pdata["TRANSLATIONPATTERN"]
                 else "*.ts"
             )
             binpattern = self.__binaryTranslationFile(pattern)
@@ -5465,24 +5521,22 @@
 
                 filetype = ""
                 bfn = os.path.basename(fn)
-                for pattern in sorted(self.pdata["FILETYPES"].keys(), reverse=True):
+                for pattern in sorted(self.__pdata["FILETYPES"].keys(), reverse=True):
                     if fnmatch.fnmatch(bfn, pattern):
-                        filetype = self.pdata["FILETYPES"][pattern]
+                        filetype = self.__pdata["FILETYPES"][pattern]
                         break
 
                 if (
-                    (filetype == "SOURCES" and fn not in self.pdata["SOURCES"])
-                    or (filetype == "FORMS" and fn not in self.pdata["FORMS"])
-                    or (filetype == "INTERFACES" and fn not in self.pdata["INTERFACES"])
-                    or (filetype == "PROTOCOLS" and fn not in self.pdata["PROTOCOLS"])
-                    or (filetype == "RESOURCES" and fn not in self.pdata["RESOURCES"])
-                    or (filetype == "OTHERS" and fn not in self.pdata["OTHERS"])
-                    or (
-                        filetype == "TRANSLATIONS"
-                        and fn not in self.pdata["TRANSLATIONS"]
-                        and (
-                            fnmatch.fnmatch(ns, pattern)
-                            or fnmatch.fnmatch(ns, binpattern)
+                    filetype in self.getFileCategories()
+                    and fn not in self.__pdata[filetype]
+                    and (
+                        filetype != "TRANSLATIONS"
+                        or (
+                            filetype == "TRANSLATIONS"
+                            and (
+                                fnmatch.fnmatch(ns, pattern)
+                                or fnmatch.fnmatch(ns, binpattern)
+                            )
                         )
                     )
                 ):
@@ -5506,8 +5560,6 @@
             return
 
         # autoInclude is not set, show a dialog
-        from .AddFoundFilesDialog import AddFoundFilesDialog
-
         dlg = AddFoundFilesDialog(newFiles, self.parent(), None)
         res = dlg.exec()
 
@@ -5531,7 +5583,7 @@
         @param updateModel flag indicating an update of the model is requested
             (boolean)
         """
-        self.projectOthersAdded.emit(fn)
+        self.projectFileAdded.emit(fn, "OTHERS")
         updateModel and self.__model.addNewItem("OTHERS", fn)
 
     def getActions(self):
@@ -5600,20 +5652,22 @@
             (boolean)
         @return a reference to the vcs object
         """
+        from eric7 import VCS
+
         vcs = None
         forProject = True
         override = False
 
         if vcsSystem is None:
-            if self.pdata["VCS"] and self.pdata["VCS"] != "None":
-                vcsSystem = self.pdata["VCS"]
+            if self.__pdata["VCS"] and self.__pdata["VCS"] != "None":
+                vcsSystem = self.__pdata["VCS"]
         else:
             forProject = False
 
         if (
             forProject
-            and self.pdata["VCS"]
-            and self.pdata["VCS"] != "None"
+            and self.__pdata["VCS"]
+            and self.__pdata["VCS"] != "None"
             and self.pudata["VCSOVERRIDE"]
             and not nooverride
         ):
@@ -5621,8 +5675,6 @@
             override = True
 
         if vcsSystem is not None:
-            from eric7 import VCS
-
             try:
                 vcs = VCS.factory(vcsSystem)
             except ImportError:
@@ -5661,7 +5713,7 @@
                     )
                 vcs = None
                 if forProject:
-                    self.pdata["VCS"] = "None"
+                    self.__pdata["VCS"] = "None"
                     self.setDirty(True)
             else:
                 vcs.vcsInitConfig(self)
@@ -5670,17 +5722,15 @@
             # set the vcs options
             if vcs.vcsSupportCommandOptions():
                 with contextlib.suppress(LookupError):
-                    vcsopt = copy.deepcopy(self.pdata["VCSOPTIONS"])
+                    vcsopt = copy.deepcopy(self.__pdata["VCSOPTIONS"])
                     vcs.vcsSetOptions(vcsopt)
             # set vcs specific data
             with contextlib.suppress(LookupError):
-                vcsother = copy.deepcopy(self.pdata["VCSOTHERDATA"])
+                vcsother = copy.deepcopy(self.__pdata["VCSOTHERDATA"])
                 vcs.vcsSetOtherData(vcsother)
 
         if forProject:
             if vcs is None:
-                from eric7 import VCS
-
                 self.vcsProjectHelper = VCS.getBasicHelper(self)
                 self.vcsBasicHelper = True
             else:
@@ -5696,7 +5746,7 @@
         """
         Public method to reset the VCS.
         """
-        self.pdata["VCS"] = "None"
+        self.__pdata["VCS"] = "None"
         self.vcs = self.initVCS()
         ericApp().getObject("PluginManager").deactivateVcsPlugins()
 
@@ -5781,13 +5831,13 @@
         """
         Private slot used to calculate some code metrics for the project files.
         """
+        from eric7.DataViews.CodeMetricsDialog import CodeMetricsDialog
+
         files = [
             os.path.join(self.ppath, file)
-            for file in self.pdata["SOURCES"]
+            for file in self.__pdata["SOURCES"]
             if file.endswith(".py")
         ]
-        from eric7.DataViews.CodeMetricsDialog import CodeMetricsDialog
-
         self.codemetrics = CodeMetricsDialog()
         self.codemetrics.show()
         self.codemetrics.prepare(files)
@@ -5797,6 +5847,8 @@
         Private slot used to show the code coverage information for the
         project files.
         """
+        from eric7.DataViews.PyCoverageDialog import PyCoverageDialog
+
         fn = self.getMainScript(True)
         if fn is None:
             EricMessageBox.critical(
@@ -5829,11 +5881,9 @@
 
         files = [
             os.path.join(self.ppath, file)
-            for file in self.pdata["SOURCES"]
+            for file in self.__pdata["SOURCES"]
             if os.path.splitext(file)[1].startswith(".py")
         ]
-        from eric7.DataViews.PyCoverageDialog import PyCoverageDialog
-
         self.codecoverage = PyCoverageDialog()
         self.codecoverage.show()
         self.codecoverage.start(fn, files)
@@ -5842,6 +5892,8 @@
         """
         Private slot used to show the profiling information for the project.
         """
+        from eric7.DataViews.PyProfileDialog import PyProfileDialog
+
         fn = self.getMainScript(True)
         if fn is None:
             EricMessageBox.critical(
@@ -5872,8 +5924,6 @@
         else:
             return
 
-        from eric7.DataViews.PyProfileDialog import PyProfileDialog
-
         self.profiledata = PyProfileDialog()
         self.profiledata.show()
         self.profiledata.start(fn)
@@ -5909,6 +5959,8 @@
         """
         Public method to handle the application diagram context menu action.
         """
+        from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType
+
         res = EricMessageBox.yesNo(
             self.ui,
             self.tr("Application Diagram"),
@@ -5916,8 +5968,6 @@
             yesDefault=True,
         )
 
-        from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType
-
         self.applicationDiagram = UMLDialog(
             UMLDialogType.APPLICATION_DIAGRAM, self, self.parent(), noModules=not res
         )
@@ -6059,16 +6109,8 @@
 
         # build the list of entries
         lst_ = []
-        for key in [
-            "SOURCES",
-            "FORMS",
-            "RESOURCES",
-            "TRANSLATIONS",
-            "INTERFACES",
-            "PROTOCOLS",
-            "OTHERS",
-        ]:
-            lst_.extend(self.pdata[key])
+        for key in self.getFileCategories():
+            lst_.extend(self.__pdata[key])
         lst = []
         for entry in lst_:
             if os.path.isdir(self.getAbsolutePath(entry)):
@@ -6095,7 +6137,7 @@
 
         # write the file
         try:
-            newline = None if self.pdata["EOL"] == 0 else self.getEolString()
+            newline = None if self.__pdata["EOL"] == 0 else self.getEolString()
             with open(pkglist, "w", encoding="utf-8", newline=newline) as pkglistFile:
                 pkglistFile.write("\n".join(header) + "\n")
                 pkglistFile.write(
@@ -6114,7 +6156,7 @@
             )
             return
 
-        if "PKGLIST" not in self.pdata["OTHERS"]:
+        if "PKGLIST" not in self.__pdata["OTHERS"]:
             self.appendFile("PKGLIST")
 
     @pyqtSlot()
@@ -6124,7 +6166,7 @@
 
         @param snapshot flag indicating snapshot archives (boolean)
         """
-        if not self.pdata["MAINSCRIPT"]:
+        if not self.__pdata["MAINSCRIPT"]:
             EricMessageBox.critical(
                 self.ui,
                 self.tr("Create Plugin Archive"),
@@ -6238,7 +6280,7 @@
                 os.path.join(self.ppath, archiveName)
                 if archiveName
                 else os.path.join(
-                    self.ppath, self.pdata["MAINSCRIPT"].replace(".py", ".zip")
+                    self.ppath, self.__pdata["MAINSCRIPT"].replace(".py", ".zip")
                 )
             )
             try:
@@ -6260,16 +6302,16 @@
                 if name:
                     try:
                         self.__createZipDirEntries(os.path.split(name)[0], archiveFile)
-                        if snapshot and name == self.pdata["MAINSCRIPT"]:
+                        if snapshot and name == self.__pdata["MAINSCRIPT"]:
                             snapshotSource, version = self.__createSnapshotSource(
-                                os.path.join(self.ppath, self.pdata["MAINSCRIPT"])
+                                os.path.join(self.ppath, self.__pdata["MAINSCRIPT"])
                             )
                             archiveFile.writestr(name, snapshotSource)
                         else:
                             archiveFile.write(os.path.join(self.ppath, name), name)
-                            if name == self.pdata["MAINSCRIPT"]:
+                            if name == self.__pdata["MAINSCRIPT"]:
                                 version = self.__pluginExtractVersion(
-                                    os.path.join(self.ppath, self.pdata["MAINSCRIPT"])
+                                    os.path.join(self.ppath, self.__pdata["MAINSCRIPT"])
                                 )
                                 if archiveVersion and (
                                     self.__pluginVersionToTuple(version)
@@ -6289,7 +6331,7 @@
             archiveFile.writestr("VERSION", version.encode("utf-8"))
             archiveFile.close()
 
-            if archive not in self.pdata["OTHERS"]:
+            if archive not in self.__pdata["OTHERS"]:
                 self.appendFile(archive)
 
         progress.setValue(len(selectedLists))
@@ -6434,7 +6476,7 @@
         @return flag indicating default parameter set
         @rtype bool
         """
-        return self.pdata["MAKEPARAMS"] == {
+        return self.__pdata["MAKEPARAMS"] == {
             "MakeEnabled": False,
             "MakeExecutable": "",
             "MakeFile": "",
@@ -6450,17 +6492,19 @@
         @return flag indicating enabled make support
         @rtype bool
         """
-        return self.pdata["MAKEPARAMS"]["MakeEnabled"]
+        return self.__pdata["MAKEPARAMS"]["MakeEnabled"]
 
     @pyqtSlot()
-    def executeMake(self):
-        """
-        Public slot to execute a project specific make run (auto-run)
+    def __autoExecuteMake(self):
+        """
+        Private slot to execute a project specific make run (auto-run)
         (execute or question).
         """
-        self.__executeMake(
-            questionOnly=self.pdata["MAKEPARAMS"]["MakeTestOnly"], interactive=False
-        )
+        if Preferences.getProject("AutoExecuteMake"):
+            self.__executeMake(
+                questionOnly=self.__pdata["MAKEPARAMS"]["MakeTestOnly"],
+                interactive=False,
+            )
 
     @pyqtSlot()
     def __executeMake(self, questionOnly=False, interactive=True):
@@ -6474,31 +6518,33 @@
         @type bool
         """
         if (
-            not self.pdata["MAKEPARAMS"]["MakeEnabled"]
+            not self.__pdata["MAKEPARAMS"]["MakeEnabled"]
             or self.__makeProcess is not None
         ):
             return
 
         prog = (
-            self.pdata["MAKEPARAMS"]["MakeExecutable"]
-            if self.pdata["MAKEPARAMS"]["MakeExecutable"]
+            self.__pdata["MAKEPARAMS"]["MakeExecutable"]
+            if self.__pdata["MAKEPARAMS"]["MakeExecutable"]
             else Project.DefaultMake
         )
 
         args = []
-        if self.pdata["MAKEPARAMS"]["MakeParameters"]:
+        if self.__pdata["MAKEPARAMS"]["MakeParameters"]:
             args.extend(
-                Utilities.parseOptionString(self.pdata["MAKEPARAMS"]["MakeParameters"])
+                Utilities.parseOptionString(
+                    self.__pdata["MAKEPARAMS"]["MakeParameters"]
+                )
             )
 
-        if self.pdata["MAKEPARAMS"]["MakeFile"]:
-            args.append("--makefile={0}".format(self.pdata["MAKEPARAMS"]["MakeFile"]))
+        if self.__pdata["MAKEPARAMS"]["MakeFile"]:
+            args.append("--makefile={0}".format(self.__pdata["MAKEPARAMS"]["MakeFile"]))
 
         if questionOnly:
             args.append("--question")
 
-        if self.pdata["MAKEPARAMS"]["MakeTarget"]:
-            args.append(self.pdata["MAKEPARAMS"]["MakeTarget"])
+        if self.__pdata["MAKEPARAMS"]["MakeTarget"]:
+            args.append(self.__pdata["MAKEPARAMS"]["MakeTarget"])
 
         self.__makeProcess = QProcess(self)
         self.__makeProcess.readyReadStandardOutput.connect(self.__makeReadStdOut)
@@ -6571,11 +6617,11 @@
                 # a rebuild is needed
                 title = self.tr("Test for Changes")
 
-                if self.pdata["MAKEPARAMS"]["MakeTarget"]:
+                if self.__pdata["MAKEPARAMS"]["MakeTarget"]:
                     message = self.tr(
                         """<p>There are changes that require the configured"""
                         """ make target <b>{0}</b> to be rebuilt.</p>"""
-                    ).format(self.pdata["MAKEPARAMS"]["MakeTarget"])
+                    ).format(self.__pdata["MAKEPARAMS"]["MakeTarget"])
                 else:
                     message = self.tr(
                         """<p>There are changes that require the default"""
@@ -6620,7 +6666,7 @@
         @return flag indicating default parameter set
         @rtype bool
         """
-        return self.pdata["IDLPARAMS"] == {
+        return self.__pdata["IDLPARAMS"] == {
             "IncludeDirs": [],
             "DefinedNames": [],
             "UndefinedNames": [],
@@ -6638,7 +6684,7 @@
         @return flag indicating default parameter set
         @rtype bool
         """
-        return self.pdata["UICPARAMS"] == {
+        return self.__pdata["UICPARAMS"] == {
             "Package": "",
             "RcSuffix": "",
             "PackagesRoot": "",
@@ -6653,8 +6699,8 @@
         @return value of the given parameter
         @rtype any, None in case on non-existence
         """
-        if name in self.pdata["UICPARAMS"]:
-            return self.pdata["UICPARAMS"][name]
+        if name in self.__pdata["UICPARAMS"]:
+            return self.__pdata["UICPARAMS"][name]
         else:
             return None
 
@@ -6670,7 +6716,7 @@
         @return flag indicating default parameter set
         @rtype bool
         """
-        return self.pdata["RCCPARAMS"] == self.getDefaultRccCompilerParameters()
+        return self.__pdata["RCCPARAMS"] == self.getDefaultRccCompilerParameters()
 
     def getDefaultRccCompilerParameters(self):
         """
@@ -6698,7 +6744,7 @@
         @return flag indicating default parameter
         @rtype bool
         """
-        return self.pdata["DOCSTRING"] == ""
+        return self.__pdata["DOCSTRING"] == ""
 
     def getDocstringType(self):
         """
@@ -6707,7 +6753,7 @@
         @return configured docstring style
         @rtype str
         """
-        return self.pdata["DOCSTRING"]
+        return self.__pdata["DOCSTRING"]
 
     #########################################################################
     ## Below are methods implementing the 'SBOM' support
@@ -6724,7 +6770,7 @@
         """
         Private slot to create a SBOM file of the project dependencies.
         """
-        import CycloneDXInterface
+        import CycloneDXInterface  # __IGNORE_WARNING_I102__
 
         CycloneDXInterface.createCycloneDXFile("<project>")
 
@@ -6787,6 +6833,56 @@
             dlg.getConfiguration(saveToProject=True)
             # The data is saved to the project as a side effect.
 
+    def __performImportSortingWithIsort(self, action):
+        """
+        Private method to format the project sources import statements using the
+        'isort' tool.
+
+        Following actions are supported.
+        <ul>
+        <li>IsortFormattingAction.Format - the imports reformatting is performed</li>
+        <li>IsortFormattingAction.Check - a check is performed, if imports formatting
+            is necessary</li>
+        <li>IsortFormattingAction.Diff - a unified diff of potential imports formatting
+            changes is generated</li>
+        </ul>
+
+        @param action formatting operation to be performed
+        @type IsortFormattingAction
+        """
+        from eric7.CodeFormatting.IsortConfigurationDialog import (
+            IsortConfigurationDialog,
+        )
+        from eric7.CodeFormatting.IsortFormattingDialog import IsortFormattingDialog
+
+        if ericApp().getObject("ViewManager").checkAllDirty():
+            dlg = IsortConfigurationDialog(withProject=True)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                config = dlg.getConfiguration(saveToProject=True)
+
+                isortDialog = IsortFormattingDialog(
+                    config,
+                    self.getProjectFiles("SOURCES", normalized=True),
+                    project=self,
+                    action=action,
+                )
+                isortDialog.exec()
+
+    @pyqtSlot()
+    def __configureIsort(self):
+        """
+        Private slot to enter the parameters for formatting the import statements of the
+        project sources with 'isort'.
+        """
+        from eric7.CodeFormatting.IsortConfigurationDialog import (
+            IsortConfigurationDialog,
+        )
+
+        dlg = IsortConfigurationDialog(withProject=True, onlyProject=True)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            dlg.getConfiguration(saveToProject=True)
+            # The data is saved to the project as a side effect.
+
     #########################################################################
     ## Below are methods implementing the 'Embedded Environment' support
     #########################################################################
@@ -6820,8 +6916,8 @@
         @param value flag indicating an embedded environment
         @type bool
         """
-        if value != self.pdata["EMBEDDED_VENV"]:
-            self.pdata["EMBEDDED_VENV"] = value
+        if value != self.__pdata["EMBEDDED_VENV"]:
+            self.__pdata["EMBEDDED_VENV"] = value
             self.setDirty(True)
 
     def __initVenvConfiguration(self):
@@ -6843,6 +6939,7 @@
         @type bool (optional)
         """
         from eric7.VirtualEnv.VirtualenvExecDialog import VirtualenvExecDialog
+
         from .ProjectVenvCreationParametersDialog import (
             ProjectVenvCreationParametersDialog,
         )

eric ide

mercurial