diff -r 4e8b98454baa -r 800c432b34c8 eric7/MultiProject/MultiProjectBrowser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/MultiProject/MultiProjectBrowser.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2008 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the multi project browser. +""" + +import os +import glob + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QDialog, QMenu + +from E5Gui.E5Application import e5App +from E5Gui import E5MessageBox + +import UI.PixmapCache + + +class MultiProjectBrowser(QTreeWidget): + """ + Class implementing the multi project browser. + """ + ProjectFileNameRole = Qt.ItemDataRole.UserRole + ProjectUidRole = Qt.ItemDataRole.UserRole + 1 + + def __init__(self, multiProject, project, parent=None): + """ + Constructor + + @param multiProject reference to the multi project object + @type MultiProject + @param project reference to the project object + @type Project + @param parent parent widget + @type QWidget + """ + super().__init__(parent) + self.multiProject = multiProject + self.project = project + + self.setWindowIcon(UI.PixmapCache.getIcon("eric")) + self.setAlternatingRowColors(True) + self.setHeaderHidden(True) + self.setItemsExpandable(False) + self.setRootIsDecorated(False) + self.setSortingEnabled(True) + + self.__openingProject = False + + self.multiProject.newMultiProject.connect( + self.__newMultiProject) + self.multiProject.multiProjectOpened.connect( + self.__multiProjectOpened) + self.multiProject.multiProjectClosed.connect( + self.__multiProjectClosed) + self.multiProject.projectDataChanged.connect( + self.__projectDataChanged) + self.multiProject.projectAdded.connect( + self.__projectAdded) + self.multiProject.projectRemoved.connect( + self.__projectRemoved) + + self.project.projectOpened.connect(self.__projectOpened) + self.project.projectClosed.connect(self.__projectClosed) + + self.__createPopupMenu() + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.__contextMenuRequested) + self.itemActivated.connect(self.__openItem) + + self.setEnabled(False) + + ########################################################################### + ## Slot handling methods below + ########################################################################### + + def __newMultiProject(self): + """ + Private slot to handle the creation of a new multi project. + """ + self.clear() + self.setEnabled(True) + + def __multiProjectOpened(self): + """ + Private slot to handle the opening of a multi project. + """ + for project in self.multiProject.getProjects(): + self.__addProject(project) + + self.sortItems(0, Qt.SortOrder.AscendingOrder) + + self.setEnabled(True) + + def __multiProjectClosed(self): + """ + Private slot to handle the closing of a multi project. + """ + self.clear() + self.setEnabled(False) + + def __projectAdded(self, project): + """ + Private slot to handle the addition of a project to the multi project. + + @param project reference to the project data dictionary + """ + self.__addProject(project) + self.sortItems(0, Qt.SortOrder.AscendingOrder) + + def __projectRemoved(self, project): + """ + Private slot to handle the removal of a project from the multi project. + + @param project reference to the project data dictionary + """ + itm = self.__findProjectItem(project) + if itm: + parent = itm.parent() + parent.removeChild(itm) + del itm + if parent.childCount() == 0: + top = self.takeTopLevelItem(self.indexOfTopLevelItem(parent)) + # __IGNORE_WARNING__ + del top + + def __projectDataChanged(self, project): + """ + Private slot to handle the change of a project of the multi project. + + @param project reference to the project data dictionary + """ + itm = self.__findProjectItem(project) + if itm: + parent = itm.parent() + if parent.text(0) != project["category"]: + self.__projectRemoved(project) + self.__addProject(project) + else: + self.__setItemData(itm, project) + + self.sortItems(0, Qt.SortOrder.AscendingOrder) + + def __projectOpened(self): + """ + Private slot to handle the opening of a project. + """ + projectfile = self.project.getProjectFile() + project = { + 'name': "", + 'file': projectfile, + 'master': False, + 'description': "", + 'category': "", + 'uid': "", + } + itm = self.__findProjectItem(project) + if itm: + font = itm.font(0) + font.setBold(True) + itm.setFont(0, font) + + def __projectClosed(self): + """ + Private slot to handle the closing of a project. + """ + for topIndex in range(self.topLevelItemCount()): + topItem = self.topLevelItem(topIndex) + for childIndex in range(topItem.childCount()): + childItem = topItem.child(childIndex) + font = childItem.font(0) + font.setBold(False) + childItem.setFont(0, font) + + def __contextMenuRequested(self, coord): + """ + Private slot to show the context menu. + + @param coord the position of the mouse pointer (QPoint) + """ + itm = self.itemAt(coord) + if itm is None or itm.parent() is None: + self.__backMenu.popup(self.mapToGlobal(coord)) + else: + self.__menu.popup(self.mapToGlobal(coord)) + + def __openItem(self, itm=None): + """ + Private slot to open a project. + + @param itm reference to the project item to be opened (QTreeWidgetItem) + """ + if itm is None: + itm = self.currentItem() + if itm is None or itm.parent() is None: + return + + if not self.__openingProject: + filename = itm.data(0, MultiProjectBrowser.ProjectFileNameRole) + if filename: + self.__openingProject = True + self.multiProject.openProject(filename) + self.__openingProject = False + + ########################################################################### + ## Private methods below + ########################################################################### + + def __findCategoryItem(self, category): + """ + Private method to find the item for a category. + + @param category category to search for (string) + @return reference to the category item or None, if there is + no such item (QTreeWidgetItem or None) + """ + if category == "": + category = self.tr("Not categorized") + for index in range(self.topLevelItemCount()): + itm = self.topLevelItem(index) + if itm.text(0) == category: + return itm + + return None + + def __addProject(self, project): + """ + Private method to add a project to the list. + + @param project reference to the project data dictionary + """ + parent = self.__findCategoryItem(project['category']) + if parent is None: + if project['category']: + parent = QTreeWidgetItem(self, [project['category']]) + else: + parent = QTreeWidgetItem(self, [self.tr("Not categorized")]) + parent.setExpanded(True) + itm = QTreeWidgetItem(parent) + self.__setItemData(itm, project) + + def __setItemData(self, itm, project): + """ + Private method to set the data of a project item. + + @param itm reference to the item to be set (QTreeWidgetItem) + @param project reference to the project data dictionary + """ + itm.setText(0, project['name']) + if project['master']: + itm.setIcon(0, UI.PixmapCache.getIcon("masterProject")) + else: + itm.setIcon(0, UI.PixmapCache.getIcon("empty")) + itm.setToolTip(0, project['file']) + itm.setData(0, MultiProjectBrowser.ProjectFileNameRole, + project['file']) + itm.setData(0, MultiProjectBrowser.ProjectUidRole, project['uid']) + + def __findProjectItem(self, project): + """ + Private method to search a specific project item. + + @param project reference to the project data dictionary + @return reference to the item (QTreeWidgetItem) or None + """ + if project["uid"]: + compareData = project["uid"] + compareRole = MultiProjectBrowser.ProjectUidRole + else: + compareData = project["file"] + compareRole = MultiProjectBrowser.ProjectFileNameRole + + for topIndex in range(self.topLevelItemCount()): + topItm = self.topLevelItem(topIndex) + for childIndex in range(topItm.childCount()): + itm = topItm.child(childIndex) + data = itm.data(0, compareRole) + if data == compareData: + return itm + + return None + + def __removeProject(self): + """ + Private method to handle the Remove context menu entry. + """ + itm = self.currentItem() + if itm is not None and itm.parent() is not None: + uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) + if uid: + self.multiProject.removeProject(uid) + + def __deleteProject(self): + """ + Private method to handle the Delete context menu entry. + """ + itm = self.currentItem() + if itm is not None and itm.parent() is not None: + projectFile = itm.data(0, MultiProjectBrowser.ProjectFileNameRole) + projectPath = os.path.dirname(projectFile) + + if self.project.getProjectPath() == projectPath: + E5MessageBox.warning( + self, + self.tr("Delete Project"), + self.tr("""The current project cannot be deleted.""" + """ Please close it first.""")) + else: + projectFiles = glob.glob(os.path.join(projectPath, "*.epj")) + projectFiles += glob.glob(os.path.join(projectPath, "*.e4p")) + if not projectFiles: + # Oops, that should not happen; play it save + res = False + elif len(projectFiles) == 1: + res = E5MessageBox.yesNo( + self, + self.tr("Delete Project"), + self.tr("""<p>Shall the project <b>{0}</b> (Path:""" + """ {1}) really be deleted?</p>""").format( + itm.text(0), projectPath)) + else: + res = E5MessageBox.yesNo( + self, + self.tr("Delete Project"), + self.tr("""<p>Shall the project <b>{0}</b> (Path:""" + """ {1}) really be deleted?</p>""" + """<p><b>Warning:</b> It contains <b>{2}</b>""" + """ sub-projects.</p>""").format( + itm.text(0), projectPath, len(projectFiles))) + if res: + for subprojectFile in projectFiles: + # remove all sub-projects before deleting the directory + if subprojectFile != projectFile: + projectData = { + 'name': "", + 'file': subprojectFile, + 'master': False, + 'description': "", + 'category': "", + 'uid': "", + } + pitm = self.__findProjectItem(projectData) + if pitm: + uid = pitm.data( + 0, MultiProjectBrowser.ProjectUidRole) + if uid: + self.multiProject.removeProject(uid) + + uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) + if uid: + self.multiProject.deleteProject(uid) + + def __showProjectProperties(self): + """ + Private method to show the data of a project entry. + """ + itm = self.currentItem() + if itm is not None and itm.parent() is not None: + uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) + if uid: + project = self.multiProject.getProject(uid) + if project is not None: + from .AddProjectDialog import AddProjectDialog + dlg = AddProjectDialog( + self, project=project, + categories=self.multiProject.getCategories()) + if dlg.exec() == QDialog.DialogCode.Accepted: + (name, filename, isMaster, description, category, + uid) = dlg.getData() + project = { + 'name': name, + 'file': filename, + 'master': isMaster, + 'description': description, + 'category': category, + 'uid': uid, + } + self.multiProject.changeProjectProperties(project) + + def __addNewProject(self): + """ + Private method to add a new project entry. + """ + itm = self.currentItem() + if itm is not None: + if itm.parent() is None: + # current item is a category item + category = itm.text(0) + else: + category = itm.parent().text(0) + else: + category = "" + self.multiProject.addNewProject(category=category) + + def __copyProject(self): + """ + Private method to copy the selected project on disk. + """ + itm = self.currentItem() + if itm and itm.parent(): + # it is a project item and not a category + uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) + if uid: + self.multiProject.copyProject(uid) + + def __createPopupMenu(self): + """ + Private method to create the popup menu. + """ + self.__menu = QMenu(self) + self.__menu.addAction(self.tr("Open"), self.__openItem) + self.__menu.addAction(self.tr("Remove from Multi Project"), + self.__removeProject) + self.__menu.addAction(self.tr("Delete from Disk"), + self.__deleteProject) + self.__menu.addAction(self.tr("Properties"), + self.__showProjectProperties) + self.__menu.addSeparator() + self.__menu.addAction(self.tr("Add Project..."), + self.__addNewProject) + self.__menu.addAction(self.tr("Copy Project..."), + self.__copyProject) + self.__menu.addSeparator() + self.__menu.addAction(self.tr("Configure..."), self.__configure) + + self.__backMenu = QMenu(self) + self.__backMenu.addAction(self.tr("Add Project..."), + self.__addNewProject) + self.__backMenu.addSeparator() + self.__backMenu.addAction(self.tr("Configure..."), + self.__configure) + + def __configure(self): + """ + Private method to open the configuration dialog. + """ + e5App().getObject("UserInterface").showPreferences("multiProjectPage")