--- 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())