eric7/MultiProject/MultiProjectBrowser.py

branch
eric7
changeset 8312
800c432b34c8
parent 8218
7c09585bd960
child 8318
962bce857696
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")

eric ide

mercurial