src/eric7/MultiProject/MultiProject.py

branch
eric7
changeset 10410
da82156f44e9
parent 10403
ea3320d5e8e9
child 10412
883e4bc9bd07
--- a/src/eric7/MultiProject/MultiProject.py	Fri Dec 15 14:07:43 2023 +0100
+++ b/src/eric7/MultiProject/MultiProject.py	Fri Dec 15 15:28:54 2023 +0100
@@ -26,6 +26,7 @@
 from eric7.SystemUtilities import FileSystemUtilities, OSUtilities
 
 from .MultiProjectFile import MultiProjectFile
+from .MultiProjectProjectMeta import MultiProjectProjectMeta
 
 
 class MultiProject(QObject):
@@ -40,11 +41,11 @@
             properties were changed
     @signal showMenu(string, QMenu) emitted when a menu is about to be shown.
             The name of the menu and a reference to the menu are given.
-    @signal projectDataChanged(project data dict) emitted after a project entry
+    @signal projectDataChanged(project metadata) emitted after a project entry
             has been changed
-    @signal projectAdded(project data dict) emitted after a project entry
+    @signal projectAdded(project metadata) emitted after a project entry
             has been added
-    @signal projectRemoved(project data dict) emitted after a project entry
+    @signal projectRemoved(project metadata) emitted after a project entry
             has been removed
     @signal projectOpened(filename) emitted after the project has been opened
     """
@@ -55,19 +56,21 @@
     multiProjectClosed = pyqtSignal()
     multiProjectPropertiesChanged = pyqtSignal()
     showMenu = pyqtSignal(str, QMenu)
-    projectDataChanged = pyqtSignal(dict)
-    projectAdded = pyqtSignal(dict)
-    projectRemoved = pyqtSignal(dict)
+    projectDataChanged = pyqtSignal(MultiProjectProjectMeta)
+    projectAdded = pyqtSignal(MultiProjectProjectMeta)
+    projectRemoved = pyqtSignal(MultiProjectProjectMeta)
     projectOpened = pyqtSignal(str)
 
     def __init__(self, project, parent=None, filename=None):
         """
         Constructor
 
-        @param project reference to the project object (Project.Project)
-        @param parent parent widget (usually the ui object) (QWidget)
+        @param project reference to the project object
+        @type Project.Project
+        @param parent parent widget (usually the ui object)
+        @type QWidget
         @param filename optional filename of a multi project file to open
-            (string)
+        @type str
         """
         super().__init__(parent)
 
@@ -96,13 +99,7 @@
         self.name = ""
         self.opened = False
         self.__projects = {}
-        # dict of project info keyed by 'uid'; each info entry is a dictionary
-        # 'name'        : name of the project
-        # 'file'        : project file name
-        # 'master'      : flag indicating the main project
-        # 'description' : description of the project
-        # 'category'    : name of the group
-        # 'uid'         : unique identifier
+        # dict of project info keyed by 'uid'; each info entry is a MultiProjectProject
         self.categories = []
 
     def __loadRecent(self):
@@ -128,7 +125,8 @@
         """
         Public method to get the most recently opened multiproject.
 
-        @return path of the most recently opened multiproject (string)
+        @return path of the most recently opened multiproject
+        @rtype str
         """
         if len(self.recent):
             return self.recent[0]
@@ -141,7 +139,8 @@
 
         It emits the signal dirty(int).
 
-        @param b dirty state (boolean)
+        @param b dirty state
+        @type bool
         """
         self.__dirty = b
         self.saveAct.setEnabled(b)
@@ -151,7 +150,8 @@
         """
         Public method to return the dirty state.
 
-        @return dirty state (boolean)
+        @return dirty state
+        @rtype bool
         """
         return self.__dirty
 
@@ -159,7 +159,8 @@
         """
         Public method to return the opened state.
 
-        @return open state (boolean)
+        @return open state
+        @rtype bool
         """
         return self.opened
 
@@ -167,7 +168,8 @@
         """
         Public method to get the multi project path.
 
-        @return multi project path (string)
+        @return multi project path
+        @rtype str
         """
         return self.ppath
 
@@ -175,41 +177,25 @@
         """
         Public method to get the path of the multi project file.
 
-        @return path of the multi project file (string)
+        @return path of the multi project file
+        @rtype str
         """
         return self.pfile
 
-    def __checkFilesExist(self):
-        """
-        Private method to check, if the files in a list exist.
-
-        The project files are checked for existance in the
-        filesystem. Non existant projects are removed from the list and the
-        dirty state of the multi project is changed accordingly.
-        """
-        removelist = []
-        for key, project in self.__projects.items():
-            if not os.path.exists(project["file"]):
-                removelist.append(key)
-
-        if removelist:
-            for key in removelist:
-                del self.__projects[key]
-            self.setDirty(True)
-
     def __extractCategories(self):
         """
         Private slot to extract the categories used in the project definitions.
         """
         for project in self.__projects.values():
-            if project["category"] and project["category"] not in self.categories:
-                self.categories.append(project["category"])
+            if project.category and project.category not in self.categories:
+                self.categories.append(project.category)
 
     def getCategories(self):
         """
         Public method to get the list of defined categories.
 
-        @return list of categories (list of string)
+        @return list of categories
+        @rtype list of str
         """
         return [c for c in self.categories if c]
 
@@ -217,8 +203,10 @@
         """
         Private method to read in a multi project (.emj, .e4m, .e5m) file.
 
-        @param fn filename of the multi project file to be read (string)
+        @param fn filename of the multi project file to be read
+        @type str
         @return flag indicating success
+        @rtype bool
         """
         if os.path.splitext(fn)[1] == ".emj":
             # new JSON based format
@@ -268,7 +256,9 @@
             is used. This is the 'save' action. If fn is given, this filename
             is used instead of the one in the multi project object. This is the
             'save as' action.
+        @type str
         @return flag indicating success
+        @rtype bool
         """
         if fn is None:
             fn = self.pfile
@@ -290,9 +280,9 @@
         Public method to add a project to the multi-project.
 
         @param project dictionary containing the project data to be added
-        @type dict
+        @type MultiProjectProjectMeta
         """
-        self.__projects[project["uid"]] = project
+        self.__projects[project.uid] = project
 
     @pyqtSlot()
     def addNewProject(self, startdir="", category=""):
@@ -314,35 +304,27 @@
             self.ui, startdir=startdir, categories=self.categories, category=category
         )
         if dlg.exec() == QDialog.DialogCode.Accepted:
-            name, filename, isMain, description, category, uid = dlg.getData()
+            newProject = dlg.getProjectMetadata()
 
             # step 1: check, if project was already added
             for project in self.__projects.values():
-                if project["file"] == filename:
+                if project.file == newProject.file:
                     return
 
             # step 2: check, if main should be changed
-            if isMain:
+            if newProject.master:
                 for project in self.__projects.values():
-                    if project["master"]:
-                        project["master"] = False
+                    if project.master:
+                        project.master = False
                         self.projectDataChanged.emit(project)
                         self.setDirty(True)
                         break
 
             # step 3: add the project entry
-            project = {
-                "name": name,
-                "file": filename,
-                "master": isMain,
-                "description": description,
-                "category": category,
-                "uid": uid,
-            }
-            self.__projects[uid] = project
+            self.__projects[newProject.uid] = newProject
             if category not in self.categories:
                 self.categories.append(category)
-            self.projectAdded.emit(project)
+            self.projectAdded.emit(newProject)
             self.setDirty(True)
 
     def copyProject(self, uid):
@@ -357,7 +339,7 @@
             if not startdir:
                 startdir = Preferences.getMultiProject("Workspace")
             srcProject = self.__projects[uid]
-            srcProjectDirectory = os.path.dirname(srcProject["file"])
+            srcProjectDirectory = os.path.dirname(srcProject.file)
             dstProjectDirectory, ok = EricPathPickerDialog.getStrPath(
                 self.parent(),
                 self.tr("Copy Project"),
@@ -384,16 +366,16 @@
                     return
 
                 dstUid = QUuid.createUuid().toString()
-                dstProject = {
-                    "name": self.tr("{0} - Copy").format(srcProject["name"]),
-                    "file": os.path.join(
+                dstProject = MultiProjectProjectMeta(
+                    name=self.tr("{0} - Copy").format(srcProject["name"]),
+                    file=os.path.join(
                         dstProjectDirectory, os.path.basename(srcProject["file"])
                     ),
-                    "master": False,
-                    "description": srcProject["description"],
-                    "category": srcProject["category"],
-                    "uid": dstUid,
-                }
+                    master=False,
+                    description=srcProject.description,
+                    category=srcProject.category,
+                    uid=dstUid,
+                )
                 self.__projects[dstUid] = dstProject
                 self.projectAdded.emit(dstProject)
                 self.setDirty(True)
@@ -402,28 +384,29 @@
         """
         Public method to change the data of a project entry.
 
-        @param pro dictionary with the project data (string)
+        @param pro dictionary with the project data
+        @type str
         """
         # step 1: check, if main should be changed
-        if pro["master"]:
+        if pro.master:
             for project in self.__projects.values():
-                if project["master"]:
-                    if project["uid"] != pro["uid"]:
-                        project["master"] = False
+                if project.master:
+                    if project.uid != pro.uid:
+                        project.master = False
                         self.projectDataChanged.emit(project)
                         self.setDirty(True)
                     break
 
         # step 2: change the entry
-        project = self.__projects[pro["uid"]]
+        project = self.__projects[pro.uid]
         # project UID is not changeable via interface
-        project["file"] = pro["file"]
-        project["name"] = pro["name"]
-        project["master"] = pro["master"]
-        project["description"] = pro["description"]
-        project["category"] = pro["category"]
-        if project["category"] not in self.categories:
-            self.categories.append(project["category"])
+        project.file = pro.file
+        project.name = pro.name
+        project.master = pro.master
+        project.description = pro.description
+        project.category = pro.category
+        if project.category not in self.categories:
+            self.categories.append(project.category)
         self.projectDataChanged.emit(project)
         self.setDirty(True)
 
@@ -431,7 +414,8 @@
         """
         Public method to get all project entries.
 
-        @return list of all project entries (list of dictionaries)
+        @return list of all project entries
+        @rtype list of MultiProjectProjectMeta
         """
         return self.__projects.values()
 
@@ -441,8 +425,8 @@
 
         @param uid UID of the project to get
         @type str
-        @return dictionary containing the project data
-        @rtype dict
+        @return project metadata
+        @rtype MultiProjectProjectMeta
         """
         if uid in self.__projects:
             return self.__projects[uid]
@@ -473,7 +457,7 @@
         """
         if uid in self.__projects:
             project = self.__projects[uid]
-            projectPath = os.path.dirname(project["file"])
+            projectPath = os.path.dirname(project.file)
             shutil.rmtree(projectPath, ignore_errors=True)
 
             self.removeProject(uid)
@@ -634,7 +618,8 @@
         """
         Public method to check the dirty status and open a message window.
 
-        @return flag indicating whether this operation was successful (boolean)
+        @return flag indicating whether this operation was successful
+        @rtype bool
         """
         if self.isDirty():
             res = EricMessageBox.okToClearData(
@@ -653,7 +638,8 @@
         """
         Public slot to close the current multi project.
 
-        @return flag indicating success (boolean)
+        @return flag indicating success
+        @rtype bool
         """
         # save the list of recently opened projects
         self.__saveRecent()
@@ -668,7 +654,7 @@
         pfile = self.projectObject.getProjectFile()
         if pfile:
             for project in self.__projects.values():
-                if project["file"] == pfile:
+                if project.file == pfile:
                     if not self.projectObject.closeProject():
                         return False
                     break
@@ -823,17 +809,40 @@
         self.propsAct.triggered.connect(self.__showProperties)
         self.actions.append(self.propsAct)
 
+        self.clearRemovedAct = EricAction(
+            self.tr("Clear Out"),
+            EricPixmapCache.getIcon("clear"),
+            self.tr("Clear Out"),
+            0,
+            0,
+            self,
+            "multi_project_clearout",
+        )
+        self.clearRemovedAct.setStatusTip(
+            self.tr("Remove all projects marked as removed")
+        )
+        self.clearRemovedAct.setWhatsThis(
+            self.tr(
+                """<b>Clear Out...</b>"""
+                """<p>This removes all projects marked as removed.</p>"""
+            )
+        )
+        self.clearRemovedAct.triggered.connect(self.clearRemovedProjects)
+        self.actions.append(self.clearRemovedAct)
+
         self.closeAct.setEnabled(False)
         self.saveAct.setEnabled(False)
         self.saveasAct.setEnabled(False)
         self.addProjectAct.setEnabled(False)
         self.propsAct.setEnabled(False)
+        self.clearRemovedAct.setEnabled(False)
 
     def initMenu(self):
         """
         Public slot to initialize the multi project menu.
 
-        @return the menu generated (QMenu)
+        @return the menu generated
+        @rtype QMenu
         """
         menu = QMenu(self.tr("&Multiproject"), self.parent())
         self.recentMenu = QMenu(self.tr("Open &Recent Multiprojects"), menu)
@@ -860,6 +869,8 @@
         menu.addSeparator()
         menu.addAction(self.addProjectAct)
         menu.addSeparator()
+        menu.addAction(self.clearRemovedAct)
+        menu.addSeparator()
         menu.addAction(self.propsAct)
 
         self.menu = menu
@@ -870,8 +881,9 @@
         Public slot to initialize the multi project toolbar.
 
         @param toolbarManager reference to a toolbar manager object
-            (EricToolBarManager)
-        @return the toolbar generated (QToolBar)
+        @type EricToolBarManager
+        @return the toolbar generated
+        @rtype QToolBar
         """
         tb = QToolBar(self.tr("Multiproject"), self.ui)
         tb.setObjectName("MultiProjectToolbar")
@@ -886,6 +898,7 @@
         toolbarManager.addToolBar(tb, tb.windowTitle())
         toolbarManager.addAction(self.addProjectAct, tb.windowTitle())
         toolbarManager.addAction(self.propsAct, tb.windowTitle())
+        toolbarManager.addAction(self.clearRemovedAct, tb.windowTitle())
 
         return tb
 
@@ -937,7 +950,8 @@
         Private method to open a multi project from the list of rencently
         opened multi projects.
 
-        @param act reference to the action that triggered (QAction)
+        @param act reference to the action that triggered
+        @type QAction
         """
         file = act.data()
         if file:
@@ -954,7 +968,8 @@
         """
         Public method to get a list of all actions.
 
-        @return list of all actions (list of EricAction)
+        @return list of all actions
+        @rtype list of EricAction
         """
         return self.actions[:]
 
@@ -962,7 +977,8 @@
         """
         Public method to add actions to the list of actions.
 
-        @param actions list of actions (list of EricAction)
+        @param actions list of actions
+        @type list of EricAction
         """
         self.actions.extend(actions)
 
@@ -970,7 +986,8 @@
         """
         Public method to remove actions from the list of actions.
 
-        @param actions list of actions (list of EricAction)
+        @param actions list of actions
+        @type list of EricAction
         """
         for act in actions:
             with contextlib.suppress(ValueError):
@@ -980,8 +997,10 @@
         """
         Public method to get a reference to the main menu or a submenu.
 
-        @param menuName name of the menu (string)
-        @return reference to the requested menu (QMenu) or None
+        @param menuName name of the menu
+        @type str
+        @return reference to the requested menu
+        @rtype QMenu or None
         """
         try:
             return self.__menus[menuName]
@@ -992,7 +1011,8 @@
         """
         Public slot to open a project.
 
-        @param filename filename of the project file (string)
+        @param filename filename of the project file
+        @type str
         """
         self.projectObject.openProject(filename)
         self.projectOpened.emit(filename)
@@ -1002,15 +1022,16 @@
         Private slot to open the main project.
 
         @param reopen flag indicating, that the main project should be
-            reopened, if it has been opened already (boolean)
+            reopened, if it has been opened already
+        @type bool
         """
         for project in self.__projects.values():
-            if project["master"] and (
+            if project.master and not project.removed and (
                 reopen
                 or not self.projectObject.isOpen()
-                or self.projectObject.getProjectFile() != project["file"]
+                or self.projectObject.getProjectFile() != project.file
             ):
-                self.openProject(project["file"])
+                self.openProject(project.file)
                 return
 
     def getMainProjectFile(self):
@@ -1021,8 +1042,8 @@
         @rtype str
         """
         for project in self.__projects:
-            if project["master"]:
-                return project["file"]
+            if project.master:
+                return project.file
 
         return None
 
@@ -1030,10 +1051,43 @@
         """
         Public method to get the filenames of the dependent projects.
 
-        @return names of the dependent project files (list of strings)
+        @return names of the dependent project files
+        @rtype list of str
         """
         files = []
         for project in self.__projects.values():
-            if not project["master"]:
-                files.append(project["file"])
+            if not project.master:
+                files.append(project.file)
         return files
+
+    def __checkFilesExist(self):
+        """
+        Private method to check, if the files in a list exist.
+
+        The project files are checked for existance in the
+        filesystem. Non existant projects are removed from the list and the
+        dirty state of the multi project is changed accordingly.
+        """
+        for project in self.__projects.values():
+            project.removed = not os.path.exists(project.file)
+            self.clearRemovedAct.setEnabled(True)
+
+    @pyqtSlot()
+    def clearRemovedProjects(self):
+        """
+        Public slot to clear out all projects marked as removed.
+        """
+        for key in list(self.__projects.keys()):
+            if self.__projects[key].removed:
+                self.removeProject(key)
+
+        self.clearRemovedAct.setEnabled(False)
+
+    def hasRemovedProjects(self):
+        """
+        Public method to check for removed projects.
+
+        @return flag indicating the existence of a removed project
+        @rtype bool
+        """
+        return any(p.removed for p in self.__projects.values())

eric ide

mercurial