eric7/MultiProject/MultiProject.py

branch
eric7
changeset 8312
800c432b34c8
parent 8259
2bbec88047dd
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/MultiProject/MultiProject.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,1030 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2008 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the multi project management functionality.
+"""
+
+import os
+import shutil
+import contextlib
+
+from PyQt5.QtCore import (
+    pyqtSignal, pyqtSlot, QFileInfo, QFile, QIODevice, QObject, QUuid
+)
+from PyQt5.QtWidgets import QMenu, QApplication, QDialog, QToolBar
+
+from Globals import recentNameMultiProject
+
+from E5Gui.E5Action import E5Action, createActionGroup
+from E5Gui import E5FileDialog, E5MessageBox, E5PathPickerDialog
+from E5Gui.E5PathPickerDialog import E5PathPickerModes
+from E5Gui.E5OverrideCursor import E5OverrideCursor
+
+import UI.PixmapCache
+
+import Preferences
+import Utilities
+
+from .MultiProjectFile import MultiProjectFile
+
+
+class MultiProject(QObject):
+    """
+    Class implementing the project management functionality.
+    
+    @signal dirty(bool) emitted when the dirty state changes
+    @signal newMultiProject() emitted after a new multi project was generated
+    @signal multiProjectOpened() emitted after a multi project file was read
+    @signal multiProjectClosed() emitted after a multi project was closed
+    @signal multiProjectPropertiesChanged() emitted after the multi project
+            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
+            has been changed
+    @signal projectAdded(project data dict) emitted after a project entry
+            has been added
+    @signal projectRemoved(project data dict) emitted after a project entry
+            has been removed
+    @signal projectOpened(filename) emitted after the project has been opened
+    """
+    dirty = pyqtSignal(bool)
+    newMultiProject = pyqtSignal()
+    multiProjectOpened = pyqtSignal()
+    multiProjectClosed = pyqtSignal()
+    multiProjectPropertiesChanged = pyqtSignal()
+    showMenu = pyqtSignal(str, QMenu)
+    projectDataChanged = pyqtSignal(dict)
+    projectAdded = pyqtSignal(dict)
+    projectRemoved = pyqtSignal(dict)
+    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 filename optional filename of a multi project file to open
+            (string)
+        """
+        super().__init__(parent)
+        
+        self.ui = parent
+        self.projectObject = project
+        
+        self.__initData()
+        
+        self.__multiProjectFile = MultiProjectFile(self)
+        
+        self.recent = []
+        self.__loadRecent()
+        
+        if filename is not None:
+            self.openMultiProject(filename)
+    
+    def __initData(self):
+        """
+        Private method to initialize the multi project data part.
+        """
+        self.loaded = False     # flag for the loaded status
+        self.__dirty = False      # dirty flag
+        self.pfile = ""         # name of the multi project file
+        self.ppath = ""         # name of the multi project directory
+        self.description = ""   # description of the multi project
+        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 master
+        #                 project
+        # 'description' : description of the project
+        # 'category'    : name of the group
+        # 'uid'         : unique identifier
+        self.categories = []
+    
+    def __loadRecent(self):
+        """
+        Private method to load the recently opened multi project filenames.
+        """
+        self.recent = []
+        Preferences.Prefs.rsettings.sync()
+        rp = Preferences.Prefs.rsettings.value(recentNameMultiProject)
+        if rp is not None:
+            for f in rp:
+                if QFileInfo(f).exists():
+                    self.recent.append(f)
+    
+    def __saveRecent(self):
+        """
+        Private method to save the list of recently opened filenames.
+        """
+        Preferences.Prefs.rsettings.setValue(
+            recentNameMultiProject, self.recent)
+        Preferences.Prefs.rsettings.sync()
+    
+    def getMostRecent(self):
+        """
+        Public method to get the most recently opened multiproject.
+        
+        @return path of the most recently opened multiproject (string)
+        """
+        if len(self.recent):
+            return self.recent[0]
+        else:
+            return None
+        
+    def setDirty(self, b):
+        """
+        Public method to set the dirty state.
+        
+        It emits the signal dirty(int).
+        
+        @param b dirty state (boolean)
+        """
+        self.__dirty = b
+        self.saveAct.setEnabled(b)
+        self.dirty.emit(bool(b))
+    
+    def isDirty(self):
+        """
+        Public method to return the dirty state.
+        
+        @return dirty state (boolean)
+        """
+        return self.__dirty
+    
+    def isOpen(self):
+        """
+        Public method to return the opened state.
+        
+        @return open state (boolean)
+        """
+        return self.opened
+    
+    def getMultiProjectPath(self):
+        """
+        Public method to get the multi project path.
+        
+        @return multi project path (string)
+        """
+        return self.ppath
+    
+    def getMultiProjectFile(self):
+        """
+        Public method to get the path of the multi project file.
+        
+        @return path of the multi project file (string)
+        """
+        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'])
+    
+    def getCategories(self):
+        """
+        Public method to get the list of defined categories.
+        
+        @return list of categories (list of string)
+        """
+        return [c for c in self.categories if c]
+    
+    def __readMultiProject(self, fn):
+        """
+        Private method to read in a multi project (.emj, .e4m, .e5m) file.
+        
+        @param fn filename of the multi project file to be read (string)
+        @return flag indicating success
+        """
+        if os.path.splitext(fn)[1] == ".emj":
+            # new JSON based format
+            with E5OverrideCursor():
+                res = self.__multiProjectFile.readFile(fn)
+        else:
+            # old XML based format
+            f = QFile(fn)
+            if f.open(QIODevice.OpenModeFlag.ReadOnly):
+                with E5OverrideCursor():
+                    from E5XML.MultiProjectReader import MultiProjectReader
+                    reader = MultiProjectReader(f, self)
+                    reader.readXML()
+                    f.close()
+                res = not reader.hasError()
+            else:
+                E5MessageBox.critical(
+                    self.ui,
+                    self.tr("Read Multi Project File"),
+                    self.tr(
+                        "<p>The multi project file <b>{0}</b> could not be"
+                        " read.</p>").format(fn))
+                res = False
+        
+        if res:
+            self.pfile = os.path.abspath(fn)
+            self.ppath = os.path.abspath(os.path.dirname(fn))
+            
+            self.__extractCategories()
+            
+            # insert filename into list of recently opened multi projects
+            self.__syncRecent()
+            
+            self.name = os.path.splitext(os.path.basename(fn))[0]
+            
+            # check, if the files of the multi project still exist
+            self.__checkFilesExist()
+        
+        return res
+
+    def __writeMultiProject(self, fn=None):
+        """
+        Private method to save the multi project infos to a multi project file.
+        
+        @param fn optional filename of the multi project file to be written.
+            If fn is None, the filename stored in the multi project object
+            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.
+        @return flag indicating success
+        """
+        if fn is None:
+            fn = self.pfile
+        
+        if os.path.splitext(fn)[1] == ".emj":
+            # new JSON based format
+            res = self.__multiProjectFile.writeFile(fn)
+        else:
+            # old XML based format
+            f = QFile(fn)
+            if f.open(QIODevice.OpenModeFlag.WriteOnly):
+                from E5XML.MultiProjectWriter import MultiProjectWriter
+                MultiProjectWriter(
+                    f,
+                    self, os.path.splitext(os.path.basename(fn))[0]
+                ).writeXML()
+                res = True
+            else:
+                E5MessageBox.critical(
+                    self.ui,
+                    self.tr("Save Multi Project File"),
+                    self.tr(
+                        "<p>The multi project file <b>{0}</b> could not be "
+                        "written.</p>").format(fn))
+                res = False
+        
+        if res:
+            self.pfile = os.path.abspath(fn)
+            self.ppath = os.path.abspath(os.path.dirname(fn))
+            self.name = os.path.splitext(os.path.basename(fn))[0]
+            self.setDirty(False)
+            
+            # insert filename into list of recently opened projects
+            self.__syncRecent()
+        
+        return res
+    
+    def addProject(self, project):
+        """
+        Public method to add a project to the multi-project.
+        
+        @param project dictionary containing the project data to be added
+        @type dict
+        """
+        self.__projects[project['uid']] = project
+    
+    @pyqtSlot()
+    def addNewProject(self, startdir="", category=""):
+        """
+        Public slot used to add a new project to the multi-project.
+        
+        @param startdir start directory for the selection dialog
+        @type str
+        @param category category to be preset
+        @type str
+        """
+        from .AddProjectDialog import AddProjectDialog
+        if not startdir:
+            startdir = self.ppath
+        if not startdir:
+            startdir = Preferences.getMultiProject("Workspace")
+        dlg = AddProjectDialog(self.ui, startdir=startdir,
+                               categories=self.categories, category=category)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            name, filename, isMaster, description, category, uid = (
+                dlg.getData()
+            )
+            
+            # step 1: check, if project was already added
+            for project in self.__projects.values():
+                if project['file'] == filename:
+                    return
+            
+            # step 2: check, if master should be changed
+            if isMaster:
+                for project in self.__projects.values():
+                    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': isMaster,
+                'description': description,
+                'category': category,
+                'uid': uid,
+            }
+            self.__projects[uid] = project
+            if category not in self.categories:
+                self.categories.append(category)
+            self.projectAdded.emit(project)
+            self.setDirty(True)
+    
+    def copyProject(self, uid):
+        """
+        Public method to copy the project with given UID on disk.
+        
+        @param uid UID of the project to copy
+        @type str
+        """
+        if uid in self.__projects:
+            startdir = self.ppath
+            if not startdir:
+                startdir = Preferences.getMultiProject("Workspace")
+            srcProject = self.__projects[uid]
+            srcProjectDirectory = os.path.dirname(srcProject["file"])
+            dstProjectDirectory, ok = E5PathPickerDialog.getPath(
+                self.parent(),
+                self.tr("Copy Project"),
+                self.tr("Enter directory for the new project (must not exist"
+                        " already):"),
+                mode=E5PathPickerModes.DirectoryMode,
+                path=srcProjectDirectory,
+                defaultDirectory=startdir,
+            )
+            if (
+                ok and
+                dstProjectDirectory and
+                not os.path.exists(dstProjectDirectory)
+            ):
+                try:
+                    shutil.copytree(srcProjectDirectory, dstProjectDirectory)
+                except shutil.Error:
+                    E5MessageBox.critical(
+                        self.parent(),
+                        self.tr("Copy Project"),
+                        self.tr("<p>The source project <b>{0}</b> could not"
+                                " be copied to its destination <b>{1}</b>."
+                                "</p>").format(srcProjectDirectory,
+                                               dstProjectDirectory))
+                    return
+                
+                dstUid = QUuid.createUuid().toString()
+                dstProject = {
+                    '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,
+                }
+                self.__projects[dstUid] = dstProject
+                self.projectAdded.emit(dstProject)
+                self.setDirty(True)
+    
+    def changeProjectProperties(self, pro):
+        """
+        Public method to change the data of a project entry.
+        
+        @param pro dictionary with the project data (string)
+        """
+        # step 1: check, if master should be changed
+        if pro['master']:
+            for project in self.__projects.values():
+                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 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'])
+        self.projectDataChanged.emit(project)
+        self.setDirty(True)
+    
+    def getProjects(self):
+        """
+        Public method to get all project entries.
+        
+        @return list of all project entries (list of dictionaries)
+        """
+        return self.__projects.values()
+    
+    def getProject(self, uid):
+        """
+        Public method to get a reference to a project entry.
+        
+        @param uid UID of the project to get
+        @type str
+        @return dictionary containing the project data
+        @rtype dict
+        """
+        if uid in self.__projects:
+            return self.__projects[uid]
+        else:
+            return None
+    
+    def removeProject(self, uid):
+        """
+        Public slot to remove a project from the multi project.
+        
+        @param uid UID of the project to be removed from the multi
+            project
+        @type str
+        """
+        if uid in self.__projects:
+            project = self.__projects[uid]
+            del self.__projects[uid]
+            self.projectRemoved.emit(project)
+            self.setDirty(True)
+    
+    def deleteProject(self, uid):
+        """
+        Public slot to delete project(s) from the multi project and disk.
+        
+        @param uid UID of the project to be removed from the multi
+            project
+        @type str
+        """
+        if uid in self.__projects:
+            project = self.__projects[uid]
+            projectPath = os.path.dirname(project["file"])
+            shutil.rmtree(projectPath, True)
+            
+            self.removeProject(uid)
+    
+    def __newMultiProject(self):
+        """
+        Private slot to build a new multi project.
+        
+        This method displays the new multi project dialog and initializes
+        the multi project object with the data entered.
+        """
+        if not self.checkDirty():
+            return
+            
+        from .PropertiesDialog import PropertiesDialog
+        dlg = PropertiesDialog(self, True)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            self.closeMultiProject()
+            dlg.storeData()
+            self.opened = True
+            self.setDirty(True)
+            self.closeAct.setEnabled(True)
+            self.saveasAct.setEnabled(True)
+            self.addProjectAct.setEnabled(True)
+            self.propsAct.setEnabled(True)
+            self.newMultiProject.emit()
+    
+    def __showProperties(self):
+        """
+        Private slot to display the properties dialog.
+        """
+        from .PropertiesDialog import PropertiesDialog
+        dlg = PropertiesDialog(self, False)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            dlg.storeData()
+            self.setDirty(True)
+            self.multiProjectPropertiesChanged.emit()
+    
+    @pyqtSlot()
+    @pyqtSlot(str)
+    def openMultiProject(self, fn=None, openMaster=True):
+        """
+        Public slot to open a multi project.
+        
+        @param fn optional filename of the multi project file to be
+            read
+        @type str
+        @param openMaster flag indicating, that the master project
+            should be opened depending on the configuration
+        @type bool
+        """
+        if not self.checkDirty():
+            return
+        
+        if fn is None:
+            fn = E5FileDialog.getOpenFileName(
+                self.parent(),
+                self.tr("Open Multi Project"),
+                Preferences.getMultiProject("Workspace") or
+                Utilities.getHomeDir(),
+                self.tr("Multi Project Files (*.emj);;"
+                        "XML Multi Project Files (*.e5m *.e4m)"))
+            
+            if fn == "":
+                fn = None
+        
+        QApplication.processEvents()
+        
+        if fn is not None:
+            self.closeMultiProject()
+            ok = self.__readMultiProject(fn)
+            if ok:
+                self.opened = True
+                
+                self.closeAct.setEnabled(True)
+                self.saveasAct.setEnabled(True)
+                self.addProjectAct.setEnabled(True)
+                self.propsAct.setEnabled(True)
+                
+                self.multiProjectOpened.emit()
+                
+                if openMaster and Preferences.getMultiProject(
+                        "OpenMasterAutomatically"):
+                    self.__openMasterProject(False)
+    
+    def saveMultiProject(self):
+        """
+        Public slot to save the current multi project.
+        
+        @return flag indicating success
+        @rtype bool
+        """
+        if self.isDirty():
+            if len(self.pfile) > 0:
+                if self.pfile.endswith((".e4m", ".e5m")):
+                    self.pfile = (self.pfile
+                                  .replace(".e4m", ".emj")
+                                  .replace(".e5m", ".emj"))
+                    self.__syncRecent()
+                ok = self.__writeMultiProject()
+            else:
+                ok = self.saveMultiProjectAs()
+        else:
+            ok = True
+        return ok
+    
+    def saveMultiProjectAs(self):
+        """
+        Public slot to save the current multi project to a different file.
+        
+        @return flag indicating success
+        @rtype bool
+        """
+        defaultFilter = self.tr("Multi Project Files (*.emj)")
+        defaultPath = (
+            self.ppath
+            if self.ppath else
+            (Preferences.getMultiProject("Workspace") or
+             Utilities.getHomeDir())
+        )
+        fn, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
+            self.parent(),
+            self.tr("Save multiproject as"),
+            defaultPath,
+            self.tr("Multi Project Files (*.emj);;"
+                    "XML Multi Project Files (*.e5m)"),
+            defaultFilter,
+            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
+        
+        if fn:
+            ext = QFileInfo(fn).suffix()
+            if not ext:
+                ex = selectedFilter.split("(*")[1].split(")")[0]
+                if ex:
+                    fn += ex
+            if QFileInfo(fn).exists():
+                res = E5MessageBox.yesNo(
+                    self.parent(),
+                    self.tr("Save File"),
+                    self.tr("<p>The file <b>{0}</b> already exists."
+                            " Overwrite it?</p>").format(fn),
+                    icon=E5MessageBox.Warning)
+                if not res:
+                    return False
+                
+            self.name = QFileInfo(fn).baseName()
+            self.__writeMultiProject(fn)
+            
+            self.multiProjectClosed.emit()
+            self.multiProjectOpened.emit()
+            return True
+        else:
+            return False
+    
+    def checkDirty(self):
+        """
+        Public method to check the dirty status and open a message window.
+        
+        @return flag indicating whether this operation was successful (boolean)
+        """
+        if self.isDirty():
+            res = E5MessageBox.okToClearData(
+                self.parent(),
+                self.tr("Close Multiproject"),
+                self.tr("The current multiproject has unsaved changes."),
+                self.saveMultiProject)
+            if res:
+                self.setDirty(False)
+            return res
+        
+        return True
+    
+    def closeMultiProject(self):
+        """
+        Public slot to close the current multi project.
+        
+        @return flag indicating success (boolean)
+        """
+        # save the list of recently opened projects
+        self.__saveRecent()
+        
+        if not self.isOpen():
+            return True
+        
+        if not self.checkDirty():
+            return False
+        
+        # now close the current project, if it belongs to the multi project
+        pfile = self.projectObject.getProjectFile()
+        if pfile:
+            for project in self.__projects.values():
+                if project['file'] == pfile:
+                    if not self.projectObject.closeProject():
+                        return False
+                    break
+        
+        self.__initData()
+        self.closeAct.setEnabled(False)
+        self.saveasAct.setEnabled(False)
+        self.saveAct.setEnabled(False)
+        self.addProjectAct.setEnabled(False)
+        self.propsAct.setEnabled(False)
+        
+        self.multiProjectClosed.emit()
+        
+        return True
+
+    def initActions(self):
+        """
+        Public slot to initialize the multi project related actions.
+        """
+        self.actions = []
+        
+        self.actGrp1 = createActionGroup(self)
+        
+        act = E5Action(
+            self.tr('New multiproject'),
+            UI.PixmapCache.getIcon("multiProjectNew"),
+            self.tr('&New...'), 0, 0,
+            self.actGrp1, 'multi_project_new')
+        act.setStatusTip(self.tr('Generate a new multiproject'))
+        act.setWhatsThis(self.tr(
+            """<b>New...</b>"""
+            """<p>This opens a dialog for entering the info for a"""
+            """ new multiproject.</p>"""
+        ))
+        act.triggered.connect(self.__newMultiProject)
+        self.actions.append(act)
+
+        act = E5Action(
+            self.tr('Open multiproject'),
+            UI.PixmapCache.getIcon("multiProjectOpen"),
+            self.tr('&Open...'), 0, 0,
+            self.actGrp1, 'multi_project_open')
+        act.setStatusTip(self.tr('Open an existing multiproject'))
+        act.setWhatsThis(self.tr(
+            """<b>Open...</b>"""
+            """<p>This opens an existing multiproject.</p>"""
+        ))
+        act.triggered.connect(self.openMultiProject)
+        self.actions.append(act)
+
+        self.closeAct = E5Action(
+            self.tr('Close multiproject'),
+            UI.PixmapCache.getIcon("multiProjectClose"),
+            self.tr('&Close'), 0, 0, self, 'multi_project_close')
+        self.closeAct.setStatusTip(self.tr(
+            'Close the current multiproject'))
+        self.closeAct.setWhatsThis(self.tr(
+            """<b>Close</b>"""
+            """<p>This closes the current multiproject.</p>"""
+        ))
+        self.closeAct.triggered.connect(self.closeMultiProject)
+        self.actions.append(self.closeAct)
+
+        self.saveAct = E5Action(
+            self.tr('Save multiproject'),
+            UI.PixmapCache.getIcon("multiProjectSave"),
+            self.tr('&Save'), 0, 0, self, 'multi_project_save')
+        self.saveAct.setStatusTip(self.tr('Save the current multiproject'))
+        self.saveAct.setWhatsThis(self.tr(
+            """<b>Save</b>"""
+            """<p>This saves the current multiproject.</p>"""
+        ))
+        self.saveAct.triggered.connect(self.saveMultiProject)
+        self.actions.append(self.saveAct)
+
+        self.saveasAct = E5Action(
+            self.tr('Save multiproject as'),
+            UI.PixmapCache.getIcon("multiProjectSaveAs"),
+            self.tr('Save &as...'), 0, 0, self,
+            'multi_project_save_as')
+        self.saveasAct.setStatusTip(self.tr(
+            'Save the current multiproject to a new file'))
+        self.saveasAct.setWhatsThis(self.tr(
+            """<b>Save as</b>"""
+            """<p>This saves the current multiproject to a new file.</p>"""
+        ))
+        self.saveasAct.triggered.connect(self.saveMultiProjectAs)
+        self.actions.append(self.saveasAct)
+
+        self.addProjectAct = E5Action(
+            self.tr('Add project to multiproject'),
+            UI.PixmapCache.getIcon("fileProject"),
+            self.tr('Add &project...'), 0, 0,
+            self, 'multi_project_add_project')
+        self.addProjectAct.setStatusTip(self.tr(
+            'Add a project to the current multiproject'))
+        self.addProjectAct.setWhatsThis(self.tr(
+            """<b>Add project...</b>"""
+            """<p>This opens a dialog for adding a project"""
+            """ to the current multiproject.</p>"""
+        ))
+        self.addProjectAct.triggered.connect(self.addNewProject)
+        self.actions.append(self.addProjectAct)
+
+        self.propsAct = E5Action(
+            self.tr('Multiproject properties'),
+            UI.PixmapCache.getIcon("multiProjectProps"),
+            self.tr('&Properties...'), 0, 0, self,
+            'multi_project_properties')
+        self.propsAct.setStatusTip(self.tr(
+            'Show the multiproject properties'))
+        self.propsAct.setWhatsThis(self.tr(
+            """<b>Properties...</b>"""
+            """<p>This shows a dialog to edit the multiproject"""
+            """ properties.</p>"""
+        ))
+        self.propsAct.triggered.connect(self.__showProperties)
+        self.actions.append(self.propsAct)
+
+        self.closeAct.setEnabled(False)
+        self.saveAct.setEnabled(False)
+        self.saveasAct.setEnabled(False)
+        self.addProjectAct.setEnabled(False)
+        self.propsAct.setEnabled(False)
+    
+    def initMenu(self):
+        """
+        Public slot to initialize the multi project menu.
+        
+        @return the menu generated (QMenu)
+        """
+        menu = QMenu(self.tr('&Multiproject'), self.parent())
+        self.recentMenu = QMenu(self.tr('Open &Recent Multiprojects'),
+                                menu)
+        
+        self.__menus = {
+            "Main": menu,
+            "Recent": self.recentMenu,
+        }
+        
+        # connect the aboutToShow signals
+        self.recentMenu.aboutToShow.connect(self.__showContextMenuRecent)
+        self.recentMenu.triggered.connect(self.__openRecent)
+        menu.aboutToShow.connect(self.__showMenu)
+        
+        # build the main menu
+        menu.setTearOffEnabled(True)
+        menu.addActions(self.actGrp1.actions())
+        self.menuRecentAct = menu.addMenu(self.recentMenu)
+        menu.addSeparator()
+        menu.addAction(self.closeAct)
+        menu.addSeparator()
+        menu.addAction(self.saveAct)
+        menu.addAction(self.saveasAct)
+        menu.addSeparator()
+        menu.addAction(self.addProjectAct)
+        menu.addSeparator()
+        menu.addAction(self.propsAct)
+        
+        self.menu = menu
+        return menu
+    
+    def initToolbar(self, toolbarManager):
+        """
+        Public slot to initialize the multi project toolbar.
+        
+        @param toolbarManager reference to a toolbar manager object
+            (E5ToolBarManager)
+        @return the toolbar generated (QToolBar)
+        """
+        tb = QToolBar(self.tr("Multiproject"), self.ui)
+        tb.setIconSize(UI.Config.ToolBarIconSize)
+        tb.setObjectName("MultiProjectToolbar")
+        tb.setToolTip(self.tr('Multiproject'))
+        
+        tb.addActions(self.actGrp1.actions())
+        tb.addAction(self.closeAct)
+        tb.addSeparator()
+        tb.addAction(self.saveAct)
+        tb.addAction(self.saveasAct)
+        
+        toolbarManager.addToolBar(tb, tb.windowTitle())
+        toolbarManager.addAction(self.addProjectAct, tb.windowTitle())
+        toolbarManager.addAction(self.propsAct, tb.windowTitle())
+        
+        return tb
+    
+    def __showMenu(self):
+        """
+        Private method to set up the multi project menu.
+        """
+        self.menuRecentAct.setEnabled(len(self.recent) > 0)
+        
+        self.showMenu.emit("Main", self.__menus["Main"])
+    
+    def __syncRecent(self):
+        """
+        Private method to synchronize the list of recently opened multi
+        projects with the central store.
+        """
+        for recent in self.recent[:]:
+            if Utilities.samepath(self.pfile, recent):
+                self.recent.remove(recent)
+        self.recent.insert(0, self.pfile)
+        maxRecent = Preferences.getProject("RecentNumber")
+        if len(self.recent) > maxRecent:
+            self.recent = self.recent[:maxRecent]
+        self.__saveRecent()
+    
+    def __showContextMenuRecent(self):
+        """
+        Private method to set up the recent multi projects menu.
+        """
+        self.__loadRecent()
+        
+        self.recentMenu.clear()
+        
+        for idx, rp in enumerate(self.recent, start=1):
+            formatStr = '&{0:d}. {1}' if idx < 10 else '{0:d}. {1}'
+            act = self.recentMenu.addAction(
+                formatStr.format(
+                    idx,
+                    Utilities.compactPath(rp, self.ui.maxMenuFilePathLen)))
+            act.setData(rp)
+            act.setEnabled(QFileInfo(rp).exists())
+        
+        self.recentMenu.addSeparator()
+        self.recentMenu.addAction(self.tr('&Clear'), self.clearRecent)
+    
+    def __openRecent(self, act):
+        """
+        Private method to open a multi project from the list of rencently
+        opened multi projects.
+        
+        @param act reference to the action that triggered (QAction)
+        """
+        file = act.data()
+        if file:
+            self.openMultiProject(file)
+    
+    def clearRecent(self):
+        """
+        Public method to clear the recent multi projects menu.
+        """
+        self.recent = []
+        self.__saveRecent()
+    
+    def getActions(self):
+        """
+        Public method to get a list of all actions.
+        
+        @return list of all actions (list of E5Action)
+        """
+        return self.actions[:]
+    
+    def addE5Actions(self, actions):
+        """
+        Public method to add actions to the list of actions.
+        
+        @param actions list of actions (list of E5Action)
+        """
+        self.actions.extend(actions)
+    
+    def removeE5Actions(self, actions):
+        """
+        Public method to remove actions from the list of actions.
+        
+        @param actions list of actions (list of E5Action)
+        """
+        for act in actions:
+            with contextlib.suppress(ValueError):
+                self.actions.remove(act)
+    
+    def getMenu(self, menuName):
+        """
+        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
+        """
+        try:
+            return self.__menus[menuName]
+        except KeyError:
+            return None
+    
+    def openProject(self, filename):
+        """
+        Public slot to open a project.
+        
+        @param filename filename of the project file (string)
+        """
+        self.projectObject.openProject(filename)
+        self.projectOpened.emit(filename)
+    
+    def __openMasterProject(self, reopen=True):
+        """
+        Private slot to open the master project.
+        
+        @param reopen flag indicating, that the master project should be
+            reopened, if it has been opened already (boolean)
+        """
+        for project in self.__projects.values():
+            if (
+                project['master'] and
+                (reopen or
+                 not self.projectObject.isOpen() or
+                 self.projectObject.getProjectFile() != project['file'])
+            ):
+                self.openProject(project['file'])
+                return
+    
+    def getMasterProjectFile(self):
+        """
+        Public method to get the filename of the master project.
+        
+        @return name of the master project file (string)
+        """
+        for project in self.__projects:
+            if project['master']:
+                return project['file']
+        
+        return None
+    
+    def getDependantProjectFiles(self):
+        """
+        Public method to get the filenames of the dependent projects.
+        
+        @return names of the dependent project files (list of strings)
+        """
+        files = []
+        for project in self.__projects.values():
+            if not project['master']:
+                files.append(project['file'])
+        return files

eric ide

mercurial