diff -r d23e9854aea4 -r 18a7312cfdb3 src/eric7/Project/Project.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Project/Project.py Sun Jul 24 11:29:56 2022 +0200 @@ -0,0 +1,6614 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the project management functionality. +""" + +import os +import time +import shutil +import glob +import fnmatch +import copy +import zipfile +import contextlib +import pathlib + +from PyQt6.QtCore import ( + pyqtSlot, + QFile, + pyqtSignal, + QCryptographicHash, + QIODevice, + QByteArray, + QObject, + QProcess, +) +from PyQt6.QtGui import QKeySequence, QAction +from PyQt6.QtWidgets import QLineEdit, QToolBar, QDialog, QInputDialog, QMenu +from PyQt6.Qsci import QsciScintilla + +from EricWidgets.EricApplication import ericApp +from EricWidgets import EricFileDialog, EricMessageBox +from EricWidgets.EricListSelectionDialog import EricListSelectionDialog +from EricWidgets.EricProgressDialog import EricProgressDialog +from EricGui.EricOverrideCursor import EricOverrideCursor, EricOverridenCursor + +from Globals import recentNameProject + +import UI.PixmapCache +from UI.NotificationWidget import NotificationTypes + +from EricGui.EricAction import EricAction, createActionGroup + +import Globals +import Preferences +import Utilities + +from .ProjectFile import ProjectFile +from .UserProjectFile import UserProjectFile +from .DebuggerPropertiesFile import DebuggerPropertiesFile + +from Sessions.SessionFile import SessionFile + +from Tasks.TasksFile import TasksFile + +from CodeFormatting.BlackFormattingAction import BlackFormattingAction + + +class Project(QObject): + """ + Class implementing the project management functionality. + + @signal dirty(bool) emitted when the dirty state changes + @signal projectLanguageAdded(str) emitted after a new language was added + @signal projectLanguageAddedByCode(str) emitted after a new language was + added. The language code is sent by this signal. + @signal projectLanguageRemoved(str) emitted after a language was removed + @signal projectFormAdded(str) emitted after a new form was added + @signal projectFormRemoved(str) emitted after a form was removed + @signal projectFormCompiled(str) emitted after a form was compiled + @signal projectSourceAdded(str) emitted after a new source file was added + @signal projectSourceRemoved(str) emitted after a source was removed + @signal projectInterfaceAdded(str) emitted after a new IDL file was added + @signal projectInterfaceRemoved(str) emitted after a IDL file was removed + @signal projectProtocolAdded(str) emitted after a new proto file was added + @signal projectProtocolRemoved(str) emitted after a proto file was removed + @signal projectResourceAdded(str) emitted after a new resource file was + added + @signal projectResourceRemoved(str) emitted after a resource was removed + @signal projectOthersAdded(str) emitted after a file or directory was added + to the OTHERS project data area + @signal projectOthersRemoved(str) emitted after a file was removed from the + OTHERS project data area + @signal projectAboutToBeCreated() emitted just before the project will be + created + @signal newProjectHooks() emitted after a new project was generated but + before the newProject() signal is sent + @signal newProject() emitted after a new project was generated + @signal sourceFile(str) emitted after a project file was read to + open the main script + @signal designerFile(str) emitted to open a found designer file + @signal linguistFile(str) emitted to open a found translation file + @signal projectOpenedHooks() emitted after a project file was read but + before the projectOpened() signal is sent + @signal projectOpened() emitted after a project file was read + @signal projectClosedHooks() emitted after a project file was closed but + before the projectClosed() signal is sent + @signal projectClosed(shutdown) emitted after a project was closed sending + a flag indicating the IDE shutdown operation + @signal projectFileRenamed(str, str) emitted after a file of the project + has been renamed + @signal projectPropertiesChanged() emitted after the project properties + were changed + @signal directoryRemoved(str) emitted after a directory has been removed + from the project + @signal prepareRepopulateItem(str) emitted before an item of the model is + repopulated + @signal completeRepopulateItem(str) emitted after an item of the model was + repopulated + @signal vcsStatusMonitorData(list) emitted to signal the VCS status data + @signal vcsStatusMonitorAllData(dict) emitted to signal all VCS status + (key is project relative file name, value is status) + @signal vcsStatusMonitorStatus(str, str) emitted to signal the status of + the monitoring thread (ok, nok, op, off) and a status message + @signal vcsStatusMonitorInfo(str) emitted to signal some info of the + monitoring thread + @signal vcsCommitted() emitted to indicate a completed commit action + @signal reinitVCS() emitted after the VCS has been reinitialized + @signal showMenu(str, QMenu) emitted when a menu is about to be shown. The + name of the menu and a reference to the menu are given. + @signal lexerAssociationsChanged() emitted after the lexer associations + have been changed + @signal projectChanged() emitted to signal a change of the project + @signal appendStdout(str) emitted after something was received from + a QProcess on stdout + @signal appendStderr(str) emitted after something was received from + a QProcess on stderr + """ + + dirty = pyqtSignal(bool) + projectLanguageAdded = pyqtSignal(str) + projectLanguageAddedByCode = pyqtSignal(str) + projectLanguageRemoved = pyqtSignal(str) + projectFormAdded = pyqtSignal(str) + projectFormRemoved = pyqtSignal(str) + projectFormCompiled = pyqtSignal(str) + projectSourceAdded = pyqtSignal(str) + projectSourceRemoved = pyqtSignal(str) + projectInterfaceAdded = pyqtSignal(str) + projectInterfaceRemoved = pyqtSignal(str) + projectProtocolAdded = pyqtSignal(str) + projectProtocolRemoved = pyqtSignal(str) + projectResourceAdded = pyqtSignal(str) + projectResourceRemoved = pyqtSignal(str) + projectOthersAdded = pyqtSignal(str) + projectOthersRemoved = pyqtSignal(str) + projectAboutToBeCreated = pyqtSignal() + newProjectHooks = pyqtSignal() + newProject = pyqtSignal() + sourceFile = pyqtSignal(str) + designerFile = pyqtSignal(str) + linguistFile = pyqtSignal(str) + projectOpenedHooks = pyqtSignal() + projectOpened = pyqtSignal() + projectClosedHooks = pyqtSignal() + projectClosed = pyqtSignal(bool) + projectFileRenamed = pyqtSignal(str, str) + projectPropertiesChanged = pyqtSignal() + directoryRemoved = pyqtSignal(str) + prepareRepopulateItem = pyqtSignal(str) + completeRepopulateItem = pyqtSignal(str) + vcsStatusMonitorData = pyqtSignal(list) + vcsStatusMonitorAllData = pyqtSignal(dict) + vcsStatusMonitorStatus = pyqtSignal(str, str) + vcsStatusMonitorInfo = pyqtSignal(str) + vcsCommitted = pyqtSignal() + reinitVCS = pyqtSignal() + showMenu = pyqtSignal(str, QMenu) + lexerAssociationsChanged = pyqtSignal() + projectChanged = pyqtSignal() + appendStdout = pyqtSignal(str) + appendStderr = pyqtSignal(str) + + eols = [os.linesep, "\n", "\r", "\r\n"] + + DefaultMake = "make" + DefaultMakefile = "makefile" + + def __init__(self, parent=None, filename=None): + """ + Constructor + + @param parent parent widget (usually the ui object) (QWidget) + @param filename optional filename of a project file to open (string) + """ + super().__init__(parent) + + self.ui = parent + + self.__progLanguages = [ + "Python3", + "MicroPython", + "Ruby", + "JavaScript", + ] + + self.__dbgFilters = { + "Python3": self.tr( + "Python3 Files (*.py *.py3);;" "Python3 GUI Files (*.pyw *.pyw3);;" + ), + } + + self.vcsMenu = None + self.__makeProcess = None + + self.__initProjectTypes() + + self.__initData() + + self.__projectFile = ProjectFile(self) + self.__userProjectFile = UserProjectFile(self) + self.__debuggerPropertiesFile = DebuggerPropertiesFile(self) + self.__sessionFile = SessionFile(False) + self.__tasksFile = TasksFile(False) + + self.recent = [] + self.__loadRecent() + + if filename is not None: + self.openProject(filename) + else: + self.vcs = self.initVCS() + + from .ProjectBrowserModel import ProjectBrowserModel + + self.__model = ProjectBrowserModel(self) + + self.codemetrics = None + self.codecoverage = None + self.profiledata = None + self.applicationDiagram = None + self.loadedDiagram = None + self.__findProjectFileDialog = None + + def __sourceExtensions(self, language): + """ + Private method to get the source extensions of a programming language. + + @param language programming language (string) + @return source extensions (list of string) + """ + if language == "Python3": + extensions = Preferences.getPython("Python3Extensions") + # *.py and *.pyw should always be associated with source files + for ext in [".py", ".pyw"]: + if ext not in extensions: + extensions.append(ext) + return extensions + elif language == "MicroPython": + extensions = Preferences.getPython("Python3Extensions") + # *.py should always be associated with source files + for ext in [".py"]: + if ext not in extensions: + extensions.append(ext) + return extensions + else: + return { + "Ruby": [".rb"], + "JavaScript": [".js"], + "Mixed": (Preferences.getPython("Python3Extensions") + [".rb", ".js"]), + }.get(language, "") + + def getProgrammingLanguages(self): + """ + Public method to get the programming languages supported by project. + + @return list of supported programming languages (list of string) + """ + return self.__progLanguages[:] + + def getDebuggerFilters(self, language): + """ + Public method to get the debugger filters for a programming language. + + @param language programming language + @type str + @return filter string + @rtype str + """ + try: + return self.__dbgFilters[language] + except KeyError: + return "" + + def __initProjectTypes(self): + """ + Private method to initialize the list of supported project types. + """ + self.__fileTypeCallbacks = {} + self.__lexerAssociationCallbacks = {} + self.__binaryTranslationsCallbacks = {} + + self.__projectTypes = { + "PyQt5": self.tr("PyQt5 GUI"), + "PyQt5C": self.tr("PyQt5 Console"), + "PyQt6": self.tr("PyQt6 GUI"), + "PyQt6C": self.tr("PyQt6 Console"), + "E7Plugin": self.tr("Eric7 Plugin"), + "Console": self.tr("Console"), + "Other": self.tr("Other"), + } + + self.__projectProgLanguages = { + "Python3": [ + "PyQt5", + "PyQt5C", + "PyQt6", + "PyQt6C", + "E7Plugin", + "Console", + "Other", + ], + "MicroPython": ["Console", "Other"], + "Ruby": ["Console", "Other"], + "JavaScript": ["Other"], + } + + if Utilities.checkPyside(variant=2): + self.__projectTypes["PySide2"] = self.tr("PySide2 GUI") + self.__projectTypes["PySide2C"] = self.tr("PySide2 Console") + self.__projectProgLanguages["Python3"].extend(["PySide2", "PySide2C"]) + + if Utilities.checkPyside(variant=6): + self.__projectTypes["PySide6"] = self.tr("PySide6 GUI") + self.__projectTypes["PySide6C"] = self.tr("PySide6 Console") + self.__projectProgLanguages["Python3"].extend(["PySide6", "PySide6C"]) + + def getProjectTypes(self, progLanguage=""): + """ + Public method to get the list of supported project types. + + @param progLanguage programming language to get project types for + (string) + @return reference to the dictionary of project types. + """ + if progLanguage and progLanguage in self.__projectProgLanguages: + ptypes = {} + for ptype in self.__projectProgLanguages[progLanguage]: + ptypes[ptype] = self.__projectTypes[ptype] + return ptypes + else: + return self.__projectTypes + + def hasProjectType(self, type_, progLanguage=""): + """ + Public method to check, if a project type is already registered. + + @param type_ internal type designator (string) + @param progLanguage programming language of the project type (string) + @return flag indicating presence of the project type (boolean) + """ + if progLanguage: + return ( + progLanguage in self.__projectProgLanguages + and type_ in self.__projectProgLanguages[progLanguage] + ) + else: + return type_ in self.__projectTypes + + def registerProjectType( + self, + type_, + description, + fileTypeCallback=None, + binaryTranslationsCallback=None, + lexerAssociationCallback=None, + progLanguages=None, + ): + """ + Public method to register a project type. + + @param type_ internal type designator to be registered (string) + @param description more verbose type name (display string) (string) + @param fileTypeCallback reference to a method returning a dictionary + of filetype associations. + @param binaryTranslationsCallback reference to a method returning + the name of the binary translation file given the name of the raw + translation file + @param lexerAssociationCallback reference to a method returning the + lexer type to be used for syntax highlighting given the name of + a file + @param progLanguages programming languages supported by the + project type (list of string) + """ + if progLanguages: + for progLanguage in progLanguages: + if progLanguage not in self.__projectProgLanguages: + EricMessageBox.critical( + self.ui, + self.tr("Registering Project Type"), + self.tr( + """<p>The Programming Language <b>{0}</b> is not""" + """ supported (project type: {1}).</p>""" + ).format(progLanguage, type_), + ) + return + + if type_ in self.__projectProgLanguages[progLanguage]: + EricMessageBox.critical( + self.ui, + self.tr("Registering Project Type"), + self.tr( + """<p>The Project type <b>{0}</b> is already""" + """ registered with Programming Language""" + """ <b>{1}</b>.</p>""" + ).format(type_, progLanguage), + ) + return + + if type_ in self.__projectTypes: + EricMessageBox.critical( + self.ui, + self.tr("Registering Project Type"), + self.tr( + """<p>The Project type <b>{0}</b> is already""" + """ registered.</p>""" + ).format(type_), + ) + else: + self.__projectTypes[type_] = description + self.__fileTypeCallbacks[type_] = fileTypeCallback + self.__lexerAssociationCallbacks[type_] = lexerAssociationCallback + self.__binaryTranslationsCallbacks[type_] = binaryTranslationsCallback + if progLanguages: + for progLanguage in progLanguages: + self.__projectProgLanguages[progLanguage].append(type_) + else: + # no specific programming languages given -> add to all + for progLanguage in self.__projectProgLanguages: + self.__projectProgLanguages[progLanguage].append(type_) + + def unregisterProjectType(self, type_): + """ + Public method to unregister a project type. + + @param type_ internal type designator to be unregistered (string) + """ + for progLanguage in self.__projectProgLanguages: + if type_ in self.__projectProgLanguages[progLanguage]: + self.__projectProgLanguages[progLanguage].remove(type_) + if type_ in self.__projectTypes: + del self.__projectTypes[type_] + if type_ in self.__fileTypeCallbacks: + del self.__fileTypeCallbacks[type_] + if type_ in self.__lexerAssociationCallbacks: + del self.__lexerAssociationCallbacks[type_] + if type_ in self.__binaryTranslationsCallbacks: + del self.__binaryTranslationsCallbacks[type_] + + def __initData(self): + """ + Private method to initialize the project data part. + """ + self.loaded = False # flag for the loaded status + self.__dirty = False # dirty flag + self.pfile = "" # name of the project file + self.ppath = "" # name of the project directory + self.translationsRoot = "" # the translations prefix + self.name = "" + self.opened = False + self.subdirs = [] + # record the project dir as a relative path (i.e. empty path) + self.otherssubdirs = [] + self.vcs = None + self.vcsRequested = False + self.dbgVirtualEnv = "" + self.dbgCmdline = "" + self.dbgWd = "" + self.dbgEnv = "" + self.dbgReportExceptions = True + self.dbgExcList = [] + self.dbgExcIgnoreList = [] + self.dbgAutoClearShell = True + self.dbgTracePython = False + self.dbgAutoContinue = True + self.dbgEnableMultiprocess = True + self.dbgMultiprocessNoDebug = "" + self.dbgGlobalConfigOverride = { + "enable": False, + "redirect": True, + } + + self.pdata = { + "DESCRIPTION": "", + "VERSION": "", + "SOURCES": [], + "FORMS": [], + "RESOURCES": [], + "INTERFACES": [], + "PROTOCOLS": [], + "OTHERS": [], + "TRANSLATIONS": [], + "TRANSLATIONEXCEPTIONS": [], + "TRANSLATIONPATTERN": "", + "TRANSLATIONSBINPATH": "", + "MAINSCRIPT": "", + "VCS": "None", + "VCSOPTIONS": {}, + "VCSOTHERDATA": {}, + "AUTHOR": "", + "EMAIL": "", + "HASH": "", + "PROGLANGUAGE": "Python3", + "MIXEDLANGUAGE": False, + "PROJECTTYPE": "PyQt5", + "SPELLLANGUAGE": Preferences.getEditor("SpellCheckingDefaultLanguage"), + "SPELLWORDS": "", + "SPELLEXCLUDES": "", + "FILETYPES": {}, + "LEXERASSOCS": {}, + "PROJECTTYPESPECIFICDATA": {}, + "CHECKERSPARMS": {}, + "PACKAGERSPARMS": {}, + "DOCUMENTATIONPARMS": {}, + "OTHERTOOLSPARMS": {}, + "MAKEPARAMS": { + "MakeEnabled": False, + "MakeExecutable": "", + "MakeFile": "", + "MakeTarget": "", + "MakeParameters": "", + "MakeTestOnly": True, + }, + "IDLPARAMS": { + "IncludeDirs": [], + "DefinedNames": [], + "UndefinedNames": [], + }, + "UICPARAMS": { + "Package": "", + "RcSuffix": "", + "PackagesRoot": "", + }, + "RCCPARAMS": { + "CompressionThreshold": 70, # default value + "CompressLevel": 0, # use zlib default + "CompressionDisable": False, + "PathPrefix": "", + }, + "EOL": -1, + "DOCSTRING": "", + "TESTING_FRAMEWORK": "", + "LICENSE": "", + } + + self.__initDebugProperties() + + self.pudata = { + "VCSOVERRIDE": "", + "VCSSTATUSMONITORINTERVAL": 0, + } + + self.vcs = self.initVCS() + + def getData(self, category, key): + """ + Public method to get data out of the project data store. + + @param category category of the data to get (string, one of + PROJECTTYPESPECIFICDATA, CHECKERSPARMS, PACKAGERSPARMS, + DOCUMENTATIONPARMS or OTHERTOOLSPARMS) + @param key key of the data entry to get (string). + @return a copy of the requested data or None + """ + # __IGNORE_WARNING_D202__ + if ( + category + in [ + "PROJECTTYPESPECIFICDATA", + "CHECKERSPARMS", + "PACKAGERSPARMS", + "DOCUMENTATIONPARMS", + "OTHERTOOLSPARMS", + ] + and key in self.pdata[category] + ): + return copy.deepcopy(self.pdata[category][key]) + else: + return None + + def setData(self, category, key, data): + """ + Public method to store data in the project data store. + + @param category category of the data to get (string, one of + PROJECTTYPESPECIFICDATA, CHECKERSPARMS, PACKAGERSPARMS, + DOCUMENTATIONPARMS or OTHERTOOLSPARMS) + @param key key of the data entry to get (string). + @param data data to be stored + @return flag indicating success (boolean) + """ + # __IGNORE_WARNING_D202__ + if category not in [ + "PROJECTTYPESPECIFICDATA", + "CHECKERSPARMS", + "PACKAGERSPARMS", + "DOCUMENTATIONPARMS", + "OTHERTOOLSPARMS", + ]: + return False + + # test for changes of data and save them in the project + # 1. there were none, now there are + if key not in self.pdata[category] and len(data) > 0: + self.pdata[category][key] = copy.deepcopy(data) + self.setDirty(True) + # 2. there were some, now there aren't + elif key in self.pdata[category] and len(data) == 0: + del self.pdata[category][key] + self.setDirty(True) + # 3. there were some and still are + elif key in self.pdata[category] and len(data) > 0: + if data != self.pdata[category][key]: + self.pdata[category][key] = copy.deepcopy(data) + self.setDirty(True) + # 4. there were none and none are given + else: + return False + return True + + def initFileTypes(self): + """ + Public method to initialize the filetype associations with default + values. + """ + self.pdata["FILETYPES"] = { + "*.txt": "OTHERS", + "*.md": "OTHERS", + "*.rst": "OTHERS", + "README": "OTHERS", + "README.*": "OTHERS", + "*.e4p": "OTHERS", + "*.epj": "OTHERS", + "GNUmakefile": "OTHERS", + "makefile": "OTHERS", + "Makefile": "OTHERS", + } + + # Sources + sourceKey = ( + "Mixed" if self.pdata["MIXEDLANGUAGE"] else self.pdata["PROGLANGUAGE"] + ) + for ext in self.__sourceExtensions(sourceKey): + self.pdata["FILETYPES"]["*{0}".format(ext)] = "SOURCES" + + # IDL interfaces + self.pdata["FILETYPES"]["*.idl"] = "INTERFACES" + + # Protobuf Files + self.pdata["FILETYPES"]["*.proto"] = "PROTOCOLS" + + # Forms + if self.pdata["PROJECTTYPE"] in [ + "E7Plugin", + "PyQt5", + "PyQt6", + "PySide2", + "PySide6", + ]: + self.pdata["FILETYPES"]["*.ui"] = "FORMS" + + # Resources + if self.pdata["PROJECTTYPE"] in [ + "PyQt5", + "PyQt5C", + "PySide2", + "PySide2C", + "PySide6", + "PySide6C", + ]: + self.pdata["FILETYPES"]["*.qrc"] = "RESOURCES" + + # Translations + if self.pdata["PROJECTTYPE"] in [ + "E7Plugin", + "PyQt5", + "PyQt5C", + "PyQt6", + "PyQt6C", + "PySide2", + "PySide2C", + "PySide6", + "PySide6C", + ]: + self.pdata["FILETYPES"]["*.ts"] = "TRANSLATIONS" + self.pdata["FILETYPES"]["*.qm"] = "TRANSLATIONS" + + # Project type specific ones + with contextlib.suppress(KeyError): + if self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]] is not None: + ftypes = self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]]() + self.pdata["FILETYPES"].update(ftypes) + + self.setDirty(True) + + def updateFileTypes(self): + """ + Public method to update the filetype associations with new default + values. + """ + if self.pdata["PROJECTTYPE"] in [ + "E7Plugin", + "PyQt5", + "PyQt5C", + "PyQt6", + "PyQt6C", + "PySide2", + "PySide2C", + "PySide6", + "PySide6C", + ]: + if "*.ts" not in self.pdata["FILETYPES"]: + self.pdata["FILETYPES"]["*.ts"] = "TRANSLATIONS" + if "*.qm" not in self.pdata["FILETYPES"]: + self.pdata["FILETYPES"]["*.qm"] = "TRANSLATIONS" + with contextlib.suppress(KeyError): + if self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]] is not None: + ftypes = self.__fileTypeCallbacks[self.pdata["PROJECTTYPE"]]() + for pattern, ftype in list(ftypes.items()): + if pattern not in self.pdata["FILETYPES"]: + self.pdata["FILETYPES"][pattern] = ftype + self.setDirty(True) + + def __loadRecent(self): + """ + Private method to load the recently opened project filenames. + """ + self.recent = [] + Preferences.Prefs.rsettings.sync() + rp = Preferences.Prefs.rsettings.value(recentNameProject) + if rp is not None: + for f in rp: + if pathlib.Path(f).exists(): + self.recent.append(f) + + def __saveRecent(self): + """ + Private method to save the list of recently opened filenames. + """ + Preferences.Prefs.rsettings.setValue(recentNameProject, self.recent) + Preferences.Prefs.rsettings.sync() + + def getMostRecent(self): + """ + Public method to get the most recently opened project. + + @return path of the most recently opened project (string) + """ + if len(self.recent): + return self.recent[0] + else: + return None + + def getModel(self): + """ + Public method to get a reference to the project browser model. + + @return reference to the project browser model (ProjectBrowserModel) + """ + return self.__model + + def startFileSystemMonitoring(self): + """ + Public method to (re)start monitoring the project file system. + """ + self.__model.startFileSystemMonitoring() + + def stopFileSystemMonitoring(self): + """ + Public method to stop monitoring the project file system. + """ + self.__model.stopFileSystemMonitoring() + + def getVcs(self): + """ + Public method to get a reference to the VCS object. + + @return reference to the VCS object + """ + return self.vcs + + def handlePreferencesChanged(self): + """ + Public slot used to handle the preferencesChanged signal. + """ + if self.pudata["VCSSTATUSMONITORINTERVAL"]: + self.setStatusMonitorInterval(self.pudata["VCSSTATUSMONITORINTERVAL"]) + else: + self.setStatusMonitorInterval(Preferences.getVCS("StatusMonitorInterval")) + + self.__model.preferencesChanged() + + def setDirty(self, dirty): + """ + Public method to set the dirty state. + + It emits the signal dirty(bool). + + @param dirty dirty state + @type bool + """ + self.__dirty = dirty + self.saveAct.setEnabled(dirty) + self.dirty.emit(dirty) + if self.__dirty: + self.projectChanged.emit() + + 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 __checkFilesExist(self, index): + """ + Private method to check, if the files in a list exist. + + The files in the indicated list are checked for existance in the + filesystem. Non existant files are removed from the list and the + dirty state of the project is changed accordingly. + + @param index key of the list to be checked (string) + """ + removed = False + removelist = [] + for file in self.pdata[index]: + if not os.path.exists(os.path.join(self.ppath, file)): + removelist.append(file) + removed = True + + if removed: + for file in removelist: + self.pdata[index].remove(file) + self.setDirty(True) + + def __readProject(self, fn): + """ + Private method to read in a project (.epj or .e4p) file. + + @param fn filename of the project file to be read (string) + @return flag indicating success + """ + if os.path.splitext(fn)[1] == ".epj": + # new JSON based format + with EricOverrideCursor(): + res = self.__projectFile.readFile(fn) + else: + # old XML based format + f = QFile(fn) + if f.open(QIODevice.OpenModeFlag.ReadOnly): + from EricXML.ProjectReader import ProjectReader + + reader = ProjectReader(f, self) + reader.readXML() + res = not reader.hasError() + f.close() + + # create hash value, if it doesn't have one + if reader.version.startswith("5.") and not self.pdata["HASH"]: + hashStr = str( + QCryptographicHash.hash( + QByteArray(self.ppath.encode("utf-8")), + QCryptographicHash.Algorithm.Sha1, + ).toHex(), + encoding="utf-8", + ) + self.pdata["HASH"] = hashStr + self.setDirty(True) + else: + EricMessageBox.critical( + self.ui, + self.tr("Read Project File"), + self.tr( + "<p>The 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)) + + # insert filename into list of recently opened projects + self.__syncRecent() + + if self.pdata["TRANSLATIONPATTERN"]: + self.translationsRoot = self.pdata["TRANSLATIONPATTERN"].split( + "%language%" + )[0] + elif self.pdata["MAINSCRIPT"]: + self.translationsRoot = os.path.splitext(self.pdata["MAINSCRIPT"])[0] + if os.path.isdir(os.path.join(self.ppath, self.translationsRoot)): + dn = self.translationsRoot + else: + dn = os.path.dirname(self.translationsRoot) + if dn not in self.subdirs: + self.subdirs.append(dn) + + self.name = os.path.splitext(os.path.basename(fn))[0] + + # check, if the files of the project still exist in the + # project directory + self.__checkFilesExist("SOURCES") + self.__checkFilesExist("FORMS") + self.__checkFilesExist("INTERFACES") + self.__checkFilesExist("PROTOCOLS") + self.__checkFilesExist("TRANSLATIONS") + self.__checkFilesExist("RESOURCES") + self.__checkFilesExist("OTHERS") + + # get the names of subdirectories the files are stored in + for fn in ( + self.pdata["SOURCES"] + + self.pdata["FORMS"] + + self.pdata["INTERFACES"] + + self.pdata["PROTOCOLS"] + + self.pdata["RESOURCES"] + + self.pdata["TRANSLATIONS"] + ): + dn = os.path.dirname(fn) + if dn not in self.subdirs: + self.subdirs.append(dn) + + # get the names of other subdirectories + for fn in self.pdata["OTHERS"]: + dn = os.path.dirname(fn) + if dn not in self.otherssubdirs: + self.otherssubdirs.append(dn) + + return res + + def __writeProject(self, fn=None): + """ + Private method to save the project infos to a project file. + + @param fn optional filename of the project file to be written (string). + If fn is None, the filename stored in the project object + is used. This is the 'save' action. If fn is given, this filename + is used instead of the one in the project object. This is the + 'save as' action. + @return flag indicating success + """ + if self.vcs is not None: + self.pdata["VCSOPTIONS"] = copy.deepcopy(self.vcs.vcsGetOptions()) + self.pdata["VCSOTHERDATA"] = copy.deepcopy(self.vcs.vcsGetOtherData()) + + if not self.pdata["HASH"]: + hashStr = str( + QCryptographicHash.hash( + QByteArray(self.ppath.encode("utf-8")), + QCryptographicHash.Algorithm.Sha1, + ).toHex(), + encoding="utf-8", + ) + self.pdata["HASH"] = hashStr + + if fn is None: + fn = self.pfile + + with EricOverrideCursor(): + res = self.__projectFile.writeFile(fn) + + 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 __readUserProperties(self): + """ + Private method to read in the user specific project file (.eqj or + .e4q). + """ + if self.pfile is None: + return + + fn1, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), "{0}.eqj".format(fn1)) + if os.path.exists(fn): + # try the new JSON based format first + self.__userProjectFile.readFile(fn) + else: + # try the old XML based format second + fn = os.path.join(self.getProjectManagementDir(), "{0}.e4q".format(fn1)) + if os.path.exists(fn): + f = QFile(fn) + if f.open(QIODevice.OpenModeFlag.ReadOnly): + from EricXML.UserProjectReader import UserProjectReader + + reader = UserProjectReader(f, self) + reader.readXML() + f.close() + else: + EricMessageBox.critical( + self.ui, + self.tr("Read User Project Properties"), + self.tr( + "<p>The user specific project properties file" + " <b>{0}</b> could not be read.</p>" + ).format(fn), + ) + + def __writeUserProperties(self): + """ + Private method to write the user specific project data to a JSON file. + """ + if self.pfile is None: + return + + fn, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), "{0}.eqj".format(fn)) + + with EricOverrideCursor(): + self.__userProjectFile.writeFile(fn) + + def __showContextMenuSession(self): + """ + Private slot called before the Session menu is shown. + """ + enable = True + if self.pfile is None: + enable = False + else: + fn, ext = os.path.splitext(os.path.basename(self.pfile)) + fn_new = os.path.join(self.getProjectManagementDir(), "{0}.esj".format(fn)) + fn_old = os.path.join(self.getProjectManagementDir(), "{0}.e5s".format(fn)) + enable = os.path.exists(fn_new) or os.path.exists(fn_old) + self.sessActGrp.findChild(QAction, "project_load_session").setEnabled(enable) + self.sessActGrp.findChild(QAction, "project_delete_session").setEnabled(enable) + + @pyqtSlot() + def __readSession(self, quiet=False, indicator=""): + """ + Private method to read in the project session file (.esj or .e5s). + + @param quiet flag indicating quiet operations. + If this flag is true, no errors are reported. + @param indicator indicator string (string) + """ + if self.pfile is None: + if not quiet: + EricMessageBox.critical( + self.ui, + self.tr("Read Project Session"), + self.tr("Please save the project first."), + ) + return + + fn1, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join( + self.getProjectManagementDir(), "{0}{1}.esj".format(fn1, indicator) + ) + if os.path.exists(fn): + # try the new JSON based format first + self.__sessionFile.readFile(fn) + else: + # try the old XML based format second + fn = os.path.join( + self.getProjectManagementDir(), "{0}{1}.e5s".format(fn1, indicator) + ) + if os.path.exists(fn): + f = QFile(fn) + if f.open(QIODevice.OpenModeFlag.ReadOnly): + from EricXML.SessionReader import SessionReader + + reader = SessionReader(f, False) + reader.readXML(quiet=quiet) + f.close() + else: + if not quiet: + EricMessageBox.critical( + self.ui, + self.tr("Read project session"), + self.tr( + "<p>The project session file <b>{0}</b> could" + " not be read.</p>" + ).format(fn), + ) + + @pyqtSlot() + def __writeSession(self, quiet=False, indicator=""): + """ + Private method to write the session data to an XML file (.esj). + + @param quiet flag indicating quiet operations. + If this flag is true, no errors are reported. + @param indicator indicator string (string) + """ + if self.pfile is None: + if not quiet: + EricMessageBox.critical( + self.ui, + self.tr("Save Project Session"), + self.tr("Please save the project first."), + ) + return + + fn, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join( + self.getProjectManagementDir(), "{0}{1}.esj".format(fn, indicator) + ) + + self.__sessionFile.writeFile(fn) + + def __deleteSession(self): + """ + Private method to delete the session file. + """ + if self.pfile is None: + EricMessageBox.critical( + self.ui, + self.tr("Delete Project Session"), + self.tr("Please save the project first."), + ) + return + + fname, ext = os.path.splitext(os.path.basename(self.pfile)) + + for ext in (".esj", ".e5s", ".e4s"): + fn = os.path.join( + self.getProjectManagementDir(), "{0}{1}".format(fname, ext) + ) + if os.path.exists(fn): + try: + os.remove(fn) + except OSError: + EricMessageBox.critical( + self.ui, + self.tr("Delete Project Session"), + self.tr( + "<p>The project session file <b>{0}</b> could" + " not be deleted.</p>" + ).format(fn), + ) + + def __readTasks(self): + """ + Private method to read in the project tasks file (.etj or .e6t). + """ + if self.pfile is None: + EricMessageBox.critical( + self.ui, + self.tr("Read Tasks"), + self.tr("Please save the project first."), + ) + return + + base, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), "{0}.etj".format(base)) + if os.path.exists(fn): + # try new style JSON file first + self.__tasksFile.readFile(fn) + else: + # try old style XML file second + fn = os.path.join(self.getProjectManagementDir(), "{0}.e6t".format(base)) + if os.path.exists(fn): + f = QFile(fn) + if f.open(QIODevice.OpenModeFlag.ReadOnly): + from EricXML.TasksReader import TasksReader + + reader = TasksReader(f, True) + reader.readXML() + f.close() + else: + EricMessageBox.critical( + self.ui, + self.tr("Read Tasks"), + self.tr( + "<p>The tasks file <b>{0}</b> could not be read." "</p>" + ).format(fn), + ) + + def writeTasks(self): + """ + Public method to write the tasks data to a JSON file (.etj). + """ + if self.pfile is None: + return + + fn, ext = os.path.splitext(os.path.basename(self.pfile)) + + fn = os.path.join(self.getProjectManagementDir(), "{0}.etj".format(fn)) + self.__tasksFile.writeFile(fn) + + def __showContextMenuDebugger(self): + """ + Private slot called before the Debugger menu is shown. + """ + enable = True + if self.pfile is None: + enable = False + else: + fn, ext = os.path.splitext(os.path.basename(self.pfile)) + # try new style file first + fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn)) + if not os.path.exists(fn): + # try old style file second + fn = os.path.join(self.getProjectManagementDir(), "{0}.e4d".format(fn)) + enable = os.path.exists(fn) + self.dbgActGrp.findChild( + QAction, "project_debugger_properties_load" + ).setEnabled(enable) + self.dbgActGrp.findChild( + QAction, "project_debugger_properties_delete" + ).setEnabled(enable) + + @pyqtSlot() + def __readDebugProperties(self, quiet=False): + """ + Private method to read in the project debugger properties file + (.edj or .e4d). + + @param quiet flag indicating quiet operations. + If this flag is true, no errors are reported. + """ + if self.pfile is None: + if not quiet: + EricMessageBox.critical( + self.ui, + self.tr("Read Debugger Properties"), + self.tr("Please save the project first."), + ) + return + + fn1, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn1)) + if os.path.exists(fn): + # try the new JSON based format first + if self.__debuggerPropertiesFile.readFile(fn): + self.debugPropertiesLoaded = True + self.debugPropertiesChanged = False + else: + # try the old XML based format second + fn = os.path.join(self.getProjectManagementDir(), "{0}.e4d".format(fn1)) + + f = QFile(fn) + if f.open(QIODevice.OpenModeFlag.ReadOnly): + from EricXML.DebuggerPropertiesReader import DebuggerPropertiesReader + + reader = DebuggerPropertiesReader(f, self) + reader.readXML(quiet=quiet) + f.close() + self.debugPropertiesLoaded = True + self.debugPropertiesChanged = False + else: + if not quiet: + EricMessageBox.critical( + self.ui, + self.tr("Read Debugger Properties"), + self.tr( + "<p>The project debugger properties file" + " <b>{0}</b> could not be read.</p>" + ).format(fn), + ) + + @pyqtSlot() + def __writeDebugProperties(self, quiet=False): + """ + Private method to write the project debugger properties file (.edj). + + @param quiet flag indicating quiet operations. + If this flag is true, no errors are reported. + """ + if self.pfile is None: + if not quiet: + EricMessageBox.critical( + self.ui, + self.tr("Save Debugger Properties"), + self.tr("Please save the project first."), + ) + return + + fn, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn)) + + with EricOverrideCursor(): + self.__debuggerPropertiesFile.writeFile(fn) + + def __deleteDebugProperties(self): + """ + Private method to delete the project debugger properties file + (.edj or .e4d). + """ + if self.pfile is None: + EricMessageBox.critical( + self.ui, + self.tr("Delete Debugger Properties"), + self.tr("Please save the project first."), + ) + return + + fname, ext = os.path.splitext(os.path.basename(self.pfile)) + + for ext in (".edj", ".e4d"): + fn = os.path.join( + self.getProjectManagementDir(), "{0}{1}".format(fname, ext) + ) + if os.path.exists(fn): + try: + os.remove(fn) + except OSError: + EricMessageBox.critical( + self.ui, + self.tr("Delete Debugger Properties"), + self.tr( + "<p>The project debugger properties file" + " <b>{0}</b> could not be deleted.</p>" + ).format(fn), + ) + + def __initDebugProperties(self): + """ + Private method to initialize the debug properties. + """ + self.debugPropertiesLoaded = False + self.debugPropertiesChanged = False + self.debugProperties = { + "VIRTUALENV": "", + "DEBUGCLIENT": "", + "ENVIRONMENTOVERRIDE": False, + "ENVIRONMENTSTRING": "", + "REMOTEDEBUGGER": False, + "REMOTEHOST": "", + "REMOTECOMMAND": "", + "PATHTRANSLATION": False, + "REMOTEPATH": "", + "LOCALPATH": "", + "CONSOLEDEBUGGER": False, + "CONSOLECOMMAND": "", + "REDIRECT": False, + "NOENCODING": False, + } + + def isDebugPropertiesLoaded(self): + """ + Public method to return the status of the debug properties. + + @return load status of debug properties (boolean) + """ + return self.debugPropertiesLoaded + + def __showDebugProperties(self): + """ + Private slot to display the debugger properties dialog. + """ + from .DebuggerPropertiesDialog import DebuggerPropertiesDialog + + dlg = DebuggerPropertiesDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + dlg.storeData() + + def getDebugProperty(self, key): + """ + Public method to retrieve a debugger property. + + @param key key of the property (string) + @return value of the property + """ + if key == "INTERPRETER": + return ( + ericApp() + .getObject("VirtualEnvManager") + .getVirtualenvInterpreter(self.debugProperties["VIRTUALENV"]) + ) + else: + return self.debugProperties[key] + + def setDbgInfo( + self, + venvName, + argv, + wd, + env, + excReporting, + excList, + excIgnoreList, + autoClearShell, + tracePython=None, + autoContinue=None, + enableMultiprocess=None, + multiprocessNoDebug=None, + configOverride=None, + ): + """ + Public method to set the debugging information. + + @param venvName name of the virtual environment used + @type str + @param argv command line arguments to be used + @type str + @param wd working directory + @type str + @param env environment setting + @type str + @param excReporting flag indicating the highlighting of exceptions + @type bool + @param excList list of exceptions to be highlighted + @type list of str + @param excIgnoreList list of exceptions to be ignored + @type list of str + @param autoClearShell flag indicating, that the interpreter window + should be cleared + @type bool + @param tracePython flag to indicate if the Python library should be + traced as well + @type bool + @param autoContinue flag indicating, that the debugger should not + stop at the first executable line + @type bool + @param enableMultiprocess flag indicating, that the debugger should + run in multi process mode + @type bool + @param multiprocessNoDebug list of programs not to be debugged in + multi process mode + @type str + @param configOverride dictionary containing the global config override + data + @type dict + """ + self.dbgVirtualEnv = venvName + self.dbgCmdline = argv + self.dbgWd = wd + self.dbgEnv = env + self.dbgReportExceptions = excReporting + self.dbgExcList = excList[:] # keep a copy of the list + self.dbgExcIgnoreList = excIgnoreList[:] # keep a copy of the list + self.dbgAutoClearShell = autoClearShell + if tracePython is not None: + self.dbgTracePython = tracePython + if autoContinue is not None: + self.dbgAutoContinue = autoContinue + if enableMultiprocess is not None: + self.dbgEnableMultiprocess = enableMultiprocess + if multiprocessNoDebug is not None: + self.dbgMultiprocessNoDebug = multiprocessNoDebug + if configOverride is not None: + self.dbgGlobalConfigOverride = copy.deepcopy(configOverride) + + def getTranslationPattern(self): + """ + Public method to get the translation pattern. + + @return translation pattern (string) + """ + return self.pdata["TRANSLATIONPATTERN"] + + def setTranslationPattern(self, pattern): + """ + Public method to set the translation pattern. + + @param pattern translation pattern + @type str + """ + self.pdata["TRANSLATIONPATTERN"] = pattern + + def addLanguage(self): + """ + Public slot used to add a language to the project. + """ + if not self.pdata["TRANSLATIONPATTERN"]: + EricMessageBox.critical( + self.ui, + self.tr("Add Language"), + self.tr("You have to specify a translation pattern first."), + ) + return + + from .AddLanguageDialog import AddLanguageDialog + + dlg = AddLanguageDialog(self.parent()) + if dlg.exec() == QDialog.DialogCode.Accepted: + lang = dlg.getSelectedLanguage() + if self.pdata["PROJECTTYPE"] in [ + "PyQt5", + "PyQt5C", + "PyQt6", + "PyQt6C", + "E7Plugin", + "PySide2", + "PySide2C", + "PySide6", + "PySide6C", + ]: + langFile = self.pdata["TRANSLATIONPATTERN"].replace("%language%", lang) + self.appendFile(langFile) + self.projectLanguageAddedByCode.emit(lang) + + def __binaryTranslationFile(self, langFile): + """ + Private method to calculate the filename of the binary translations + file given the name of the raw translations file. + + @param langFile name of the raw translations file (string) + @return name of the binary translations file (string) + """ + qmFile = "" + try: + if ( + self.__binaryTranslationsCallbacks[self.pdata["PROJECTTYPE"]] + is not None + ): + qmFile = self.__binaryTranslationsCallbacks[self.pdata["PROJECTTYPE"]]( + langFile + ) + except KeyError: + qmFile = langFile.replace(".ts", ".qm") + if qmFile == langFile: + qmFile = "" + return qmFile + + def checkLanguageFiles(self): + """ + Public slot to check the language files after a release process. + """ + tbPath = self.pdata["TRANSLATIONSBINPATH"] + for langFile in self.pdata["TRANSLATIONS"][:]: + qmFile = self.__binaryTranslationFile(langFile) + if qmFile: + if qmFile not in self.pdata["TRANSLATIONS"] and os.path.exists( + os.path.join(self.ppath, qmFile) + ): + self.appendFile(qmFile) + if tbPath: + qmFile = os.path.join(tbPath, os.path.basename(qmFile)) + if qmFile not in self.pdata["TRANSLATIONS"] and os.path.exists( + os.path.join(self.ppath, qmFile) + ): + self.appendFile(qmFile) + + def removeLanguageFile(self, langFile): + """ + Public slot to remove a translation from the project. + + The translation file is not deleted from the project directory. + + @param langFile the translation file to be removed (string) + """ + langFile = self.getRelativePath(langFile) + qmFile = self.__binaryTranslationFile(langFile) + self.pdata["TRANSLATIONS"].remove(langFile) + self.__model.removeItem(langFile) + if qmFile: + with contextlib.suppress(ValueError): + if self.pdata["TRANSLATIONSBINPATH"]: + qmFile = self.getRelativePath( + os.path.join( + self.pdata["TRANSLATIONSBINPATH"], os.path.basename(qmFile) + ) + ) + self.pdata["TRANSLATIONS"].remove(qmFile) + self.__model.removeItem(qmFile) + self.setDirty(True) + + def deleteLanguageFile(self, langFile): + """ + Public slot to delete a translation from the project directory. + + @param langFile the translation file to be removed (string) + """ + try: + from send2trash import send2trash as s2t + except ImportError: + s2t = os.remove + + langFile = self.getRelativePath(langFile) + qmFile = self.__binaryTranslationFile(langFile) + + try: + fn = os.path.join(self.ppath, langFile) + if os.path.exists(fn): + s2t(fn) + except OSError as err: + EricMessageBox.critical( + self.ui, + self.tr("Delete translation"), + self.tr( + "<p>The selected translation file <b>{0}</b> could not be" + " deleted.</p><p>Reason: {1}</p>" + ).format(langFile, str(err)), + ) + return + + self.removeLanguageFile(langFile) + + # now get rid of the .qm file + if qmFile: + try: + if self.pdata["TRANSLATIONSBINPATH"]: + qmFile = self.getRelativePath( + os.path.join( + self.pdata["TRANSLATIONSBINPATH"], os.path.basename(qmFile) + ) + ) + fn = os.path.join(self.ppath, qmFile) + if os.path.exists(fn): + s2t(fn) + except OSError as err: + EricMessageBox.critical( + self.ui, + self.tr("Delete translation"), + self.tr( + "<p>The selected translation file <b>{0}</b> could" + " not be deleted.</p><p>Reason: {1}</p>" + ).format(qmFile, str(err)), + ) + return + + def appendFile(self, fn, isSourceFile=False, updateModel=True): + """ + Public method to append a file to the project. + + @param fn filename to be added to the project (string) + @param isSourceFile flag indicating that this is a source file + even if it doesn't have the source extension (boolean) + @param updateModel flag indicating an update of the model is + requested (boolean) + """ + dirty = False + + # make it relative to the project root, if it starts with that path + # assume relative paths are relative to the project root + newfn = self.getRelativePath(fn) if os.path.isabs(fn) else fn + newdir = os.path.dirname(newfn) + + if isSourceFile: + filetype = "SOURCES" + else: + filetype = "OTHERS" + bfn = os.path.basename(newfn) + if fnmatch.fnmatch(bfn, "*.ts") or fnmatch.fnmatch(bfn, "*.qm"): + filetype = "TRANSLATIONS" + else: + for pattern in sorted(self.pdata["FILETYPES"].keys(), reverse=True): + if fnmatch.fnmatch(bfn, pattern): + filetype = self.pdata["FILETYPES"][pattern] + break + + if filetype == "__IGNORE__": + return + + if filetype in ["SOURCES", "FORMS", "INTERFACES", "PROTOCOLS", "RESOURCES"]: + if filetype == "SOURCES": + if newfn not in self.pdata["SOURCES"]: + self.pdata["SOURCES"].append(newfn) + self.projectSourceAdded.emit(newfn) + updateModel and self.__model.addNewItem("SOURCES", newfn) + dirty = True + else: + updateModel and self.repopulateItem(newfn) + elif filetype == "FORMS": + if newfn not in self.pdata["FORMS"]: + self.pdata["FORMS"].append(newfn) + self.projectFormAdded.emit(newfn) + updateModel and self.__model.addNewItem("FORMS", newfn) + dirty = True + else: + updateModel and self.repopulateItem(newfn) + elif filetype == "INTERFACES": + if newfn not in self.pdata["INTERFACES"]: + self.pdata["INTERFACES"].append(newfn) + self.projectInterfaceAdded.emit(newfn) + (updateModel and self.__model.addNewItem("INTERFACES", newfn)) + dirty = True + else: + updateModel and self.repopulateItem(newfn) + elif filetype == "PROTOCOLS": + if newfn not in self.pdata["PROTOCOLS"]: + self.pdata["PROTOCOLS"].append(newfn) + self.projectProtocolAdded.emit(newfn) + (updateModel and self.__model.addNewItem("PROTOCOLS", newfn)) + dirty = True + else: + updateModel and self.repopulateItem(newfn) + elif filetype == "RESOURCES": + if newfn not in self.pdata["RESOURCES"]: + self.pdata["RESOURCES"].append(newfn) + self.projectResourceAdded.emit(newfn) + updateModel and self.__model.addNewItem("RESOURCES", newfn) + dirty = True + else: + updateModel and self.repopulateItem(newfn) + if newdir not in self.subdirs: + self.subdirs.append(newdir) + elif filetype == "TRANSLATIONS": + if newfn not in self.pdata["TRANSLATIONS"]: + self.pdata["TRANSLATIONS"].append(newfn) + updateModel and self.__model.addNewItem("TRANSLATIONS", newfn) + self.projectLanguageAdded.emit(newfn) + dirty = True + else: + updateModel and self.repopulateItem(newfn) + else: # filetype == "OTHERS" + if newfn not in self.pdata["OTHERS"]: + self.pdata["OTHERS"].append(newfn) + self.othersAdded(newfn, updateModel) + dirty = True + else: + updateModel and self.repopulateItem(newfn) + if newdir not in self.otherssubdirs: + self.otherssubdirs.append(newdir) + + if dirty: + self.setDirty(True) + + @pyqtSlot() + def addFiles(self, fileTypeFilter=None, startdir=None): + """ + Public slot used to add files to the project. + + @param fileTypeFilter filter to be used by the add file dialog + @type str out of source, form, resource, interface, protocol, others + @param startdir start directory for the selection dialog + @type str + """ + if startdir is None: + startdir = self.ppath + from .AddFileDialog import AddFileDialog + + dlg = AddFileDialog(self, self.parent(), fileTypeFilter, startdir=startdir) + if dlg.exec() == QDialog.DialogCode.Accepted: + fnames, target, isSource = dlg.getData() + if target != "": + for fn in fnames: + targetfile = os.path.join(target, os.path.basename(fn)) + if not Utilities.samepath(os.path.dirname(fn), target): + try: + if not os.path.isdir(target): + os.makedirs(target) + + if os.path.exists(targetfile): + res = EricMessageBox.yesNo( + self.ui, + self.tr("Add file"), + self.tr( + "<p>The file <b>{0}</b> already" + " exists.</p><p>Overwrite it?</p>" + ).format(targetfile), + icon=EricMessageBox.Warning, + ) + if not res: + return # don't overwrite + + shutil.copy(fn, target) + except OSError as why: + EricMessageBox.critical( + self.ui, + self.tr("Add file"), + self.tr( + "<p>The selected file <b>{0}</b> could" + " not be added to <b>{1}</b>.</p>" + "<p>Reason: {2}</p>" + ).format(fn, target, str(why)), + ) + continue + + self.appendFile(targetfile, isSource or fileTypeFilter == "source") + else: + EricMessageBox.critical( + self.ui, + self.tr("Add file"), + self.tr("The target directory must not be empty."), + ) + + def __addSingleDirectory(self, filetype, source, target, quiet=False): + """ + Private method used to add all files of a single directory to the + project. + + @param filetype type of files to add (string) + @param source source directory (string) + @param target target directory (string) + @param quiet flag indicating quiet operations (boolean) + """ + # get all relevant filename patterns + patterns = [] + ignorePatterns = [] + for pattern, patterntype in list(self.pdata["FILETYPES"].items()): + if patterntype == filetype: + patterns.append(pattern) + elif patterntype == "__IGNORE__": + ignorePatterns.append(pattern) + + files = [] + for pattern in patterns: + sstring = "{0}{1}{2}".format(source, os.sep, pattern) + files.extend(glob.glob(sstring)) + + if len(files) == 0: + if not quiet: + EricMessageBox.information( + self.ui, + self.tr("Add directory"), + self.tr( + "<p>The source directory doesn't contain" + " any files belonging to the selected category.</p>" + ), + ) + return + + if not Utilities.samepath(target, source) and not os.path.isdir(target): + try: + os.makedirs(target) + except OSError as why: + EricMessageBox.critical( + self.ui, + self.tr("Add directory"), + self.tr( + "<p>The target directory <b>{0}</b> could not be" + " created.</p><p>Reason: {1}</p>" + ).format(target, str(why)), + ) + return + + for file in files: + for pattern in ignorePatterns: + if fnmatch.fnmatch(file, pattern): + continue + + targetfile = os.path.join(target, os.path.basename(file)) + if not Utilities.samepath(target, source): + try: + if os.path.exists(targetfile): + res = EricMessageBox.yesNo( + self.ui, + self.tr("Add directory"), + self.tr( + "<p>The file <b>{0}</b> already exists.</p>" + "<p>Overwrite it?</p>" + ).format(targetfile), + icon=EricMessageBox.Warning, + ) + if not res: + continue + # don't overwrite, carry on with next file + + shutil.copy(file, target) + except OSError: + continue + self.appendFile(targetfile) + + def __addRecursiveDirectory(self, filetype, source, target): + """ + Private method used to add all files of a directory tree. + + The tree is rooted at source to another one rooted at target. This + method decents down to the lowest subdirectory. + + @param filetype type of files to add (string) + @param source source directory (string) + @param target target directory (string) + """ + # first perform the addition of source + self.__addSingleDirectory(filetype, source, target, True) + + ignore_patterns = [ + pattern + for pattern, filetype in self.pdata["FILETYPES"].items() + if filetype == "__IGNORE__" + ] + + # now recurse into subdirectories + for name in os.listdir(source): + ns = os.path.join(source, name) + if os.path.isdir(ns): + skip = False + for ignore_pattern in ignore_patterns: + if fnmatch.fnmatch(name, ignore_pattern): + skip = True + break + if skip: + continue + + nt = os.path.join(target, name) + self.__addRecursiveDirectory(filetype, ns, nt) + + @pyqtSlot() + def addDirectory(self, fileTypeFilter=None, startdir=None): + """ + Public method used to add all files of a directory to the project. + + @param fileTypeFilter filter to be used by the add directory dialog + @type str out of source, form, resource, interface, protocol, others + @param startdir start directory for the selection dialog + @type str + """ + if startdir is None: + startdir = self.ppath + from .AddDirectoryDialog import AddDirectoryDialog + + dlg = AddDirectoryDialog(self, fileTypeFilter, self.parent(), startdir=startdir) + if dlg.exec() == QDialog.DialogCode.Accepted: + filetype, source, target, recursive = dlg.getData() + if target == "": + EricMessageBox.critical( + self.ui, + self.tr("Add directory"), + self.tr("The target directory must not be empty."), + ) + return + + if filetype == "OTHERS": + self.addToOthers(source) + return + + if source == "": + EricMessageBox.critical( + self.ui, + self.tr("Add directory"), + self.tr("The source directory must not be empty."), + ) + return + + if recursive: + self.__addRecursiveDirectory(filetype, source, target) + else: + self.__addSingleDirectory(filetype, source, target) + + def addToOthers(self, fn): + """ + Public method to add a file/directory to the OTHERS project data. + + @param fn file name or directory name to add (string) + """ + if fn: + # if it is below the project directory, make it relative to that + fn = self.getRelativePath(fn) + + # if it ends with the directory separator character, remove it + if fn.endswith(os.sep): + fn = fn[:-1] + + if fn not in self.pdata["OTHERS"]: + self.pdata["OTHERS"].append(fn) + self.othersAdded(fn) + self.setDirty(True) + + if os.path.isdir(fn) and fn not in self.otherssubdirs: + self.otherssubdirs.append(fn) + + def addSourceFiles(self): + """ + Public slot to add source files to the current project. + """ + self.addFiles("source") + + def addUiFiles(self): + """ + Public slot to add forms to the current project. + """ + self.addFiles("form") + + def addIdlFiles(self): + """ + Public slot to add IDL interfaces to the current project. + """ + self.addFiles("interface") + + def addProtoFiles(self): + """ + Public slot to add protocol files to the current project. + """ + self.addFiles("protocol") + + def addResourceFiles(self): + """ + Public slot to add Qt resources to the current project. + """ + self.addFiles("resource") + + def addOthersFiles(self): + """ + Public slot to add files to the OTHERS project data. + """ + self.addFiles("others") + + def addSourceDir(self): + """ + Public slot to add all source files of a directory to the current + project. + """ + self.addDirectory("source") + + def addUiDir(self): + """ + Public slot to add all forms of a directory to the current project. + """ + self.addDirectory("form") + + def addIdlDir(self): + """ + Public slot to add all IDL interfaces of a directory to the current + project. + """ + self.addDirectory("interface") + + def addProtoDir(self): + """ + Public slot to add all protocol files of a directory to the current + project. + """ + self.addDirectory("protocol") + + def addResourceDir(self): + """ + Public slot to add all Qt resource files of a directory to the current + project. + """ + self.addDirectory("resource") + + def addOthersDir(self): + """ + Public slot to add a directory to the OTHERS project data. + """ + self.addDirectory("others") + + def renameMainScript(self, oldfn, newfn): + """ + Public method to rename the main script. + + @param oldfn old filename (string) + @param newfn new filename of the main script (string) + """ + if self.pdata["MAINSCRIPT"]: + ofn = self.getRelativePath(oldfn) + if ofn != self.pdata["MAINSCRIPT"]: + return + + fn = self.getRelativePath(newfn) + self.pdata["MAINSCRIPT"] = fn + self.setDirty(True) + + def renameFile(self, oldfn, newfn=None): + """ + Public slot to rename a file of the project. + + @param oldfn old filename of the file (string) + @param newfn new filename of the file (string) + @return flag indicating success + """ + fn = self.getRelativePath(oldfn) + isSourceFile = fn in self.pdata["SOURCES"] + + if newfn is None: + newfn = EricFileDialog.getSaveFileName( + None, + self.tr("Rename file"), + oldfn, + "", + EricFileDialog.DontConfirmOverwrite, + ) + if not newfn: + return False + newfn = Utilities.toNativeSeparators(newfn) + + if os.path.exists(newfn): + res = EricMessageBox.yesNo( + self.ui, + self.tr("Rename File"), + self.tr( + """<p>The file <b>{0}</b> already exists.""" + """ Overwrite it?</p>""" + ).format(newfn), + icon=EricMessageBox.Warning, + ) + if not res: + return False + + try: + os.rename(oldfn, newfn) + except OSError as msg: + EricMessageBox.critical( + self.ui, + self.tr("Rename File"), + self.tr( + """<p>The file <b>{0}</b> could not be renamed.<br />""" + """Reason: {1}</p>""" + ).format(oldfn, str(msg)), + ) + return False + + if ( + fn in self.pdata["SOURCES"] + or fn in self.pdata["FORMS"] + or fn in self.pdata["TRANSLATIONS"] + or fn in self.pdata["INTERFACES"] + or fn in self.pdata["PROTOCOLS"] + or fn in self.pdata["RESOURCES"] + or fn in self.pdata["OTHERS"] + ): + self.renameFileInPdata(oldfn, newfn, isSourceFile) + + return True + + def renameFileInPdata(self, oldname, newname, isSourceFile=False): + """ + Public method to rename a file in the pdata structure. + + @param oldname old filename (string) + @param newname new filename (string) + @param isSourceFile flag indicating that this is a source file + even if it doesn't have the source extension (boolean) + """ + fn = self.getRelativePath(oldname) + if os.path.dirname(oldname) == os.path.dirname(newname): + if self.__isInPdata(oldname): + self.removeFile(oldname, False) + self.appendFile(newname, isSourceFile, False) + self.__model.renameItem(fn, newname) + else: + self.removeFile(oldname) + self.appendFile(newname, isSourceFile) + self.projectFileRenamed.emit(oldname, newname) + + self.renameMainScript(fn, newname) + + def getFiles(self, start): + """ + Public method to get all files starting with a common prefix. + + @param start prefix (string) + @return list of files starting with a common prefix (list of strings) + """ + filelist = [] + start = self.getRelativePath(start) + for key in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "OTHERS", + ]: + for entry in self.pdata[key][:]: + if entry.startswith(start): + filelist.append(os.path.join(self.ppath, entry)) + return filelist + + def __reorganizeFiles(self): + """ + Private method to reorganize files stored in the project. + """ + reorganized = False + + # init data store for the reorganization + newPdata = {} + for key in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "OTHERS", + "TRANSLATIONS", + ]: + newPdata[key] = [] + + # iterate over all files checking for a reassignment + for key in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "OTHERS", + "TRANSLATIONS", + ]: + for fn in self.pdata[key][:]: + filetype = key + bfn = os.path.basename(fn) + for pattern in sorted(self.pdata["FILETYPES"].keys(), reverse=True): + if fnmatch.fnmatch(bfn, pattern): + filetype = self.pdata["FILETYPES"][pattern] + break + + if filetype != "__IGNORE__": + newPdata[filetype].append(fn) + if filetype != key: + reorganized = True + + if reorganized: + # copy the reorganized files back to the project + for key in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "OTHERS", + "TRANSLATIONS", + ]: + self.pdata[key] = newPdata[key][:] + + # repopulate the model + self.__model.projectClosed(False) + self.__model.projectOpened() + + def copyDirectory(self, olddn, newdn): + """ + Public slot to copy a directory. + + @param olddn original directory name (string) + @param newdn new directory name (string) + """ + olddn = self.getRelativePath(olddn) + newdn = self.getRelativePath(newdn) + for key in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "OTHERS", + ]: + for entry in self.pdata[key][:]: + if entry.startswith(olddn): + entry = entry.replace(olddn, newdn) + self.appendFile(os.path.join(self.ppath, entry), key == "SOURCES") + self.setDirty(True) + + def moveDirectory(self, olddn, newdn): + """ + Public slot to move a directory. + + @param olddn old directory name (string) + @param newdn new directory name (string) + """ + olddn = self.getRelativePath(olddn) + newdn = self.getRelativePath(newdn) + typeStrings = [] + for key in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "OTHERS", + ]: + for entry in self.pdata[key][:]: + if entry.startswith(olddn): + if key not in typeStrings: + typeStrings.append(key) + self.pdata[key].remove(entry) + entry = entry.replace(olddn, newdn) + self.pdata[key].append(entry) + if key == "OTHERS": + if newdn not in self.otherssubdirs: + self.otherssubdirs.append(newdn) + else: + if newdn not in self.subdirs: + self.subdirs.append(newdn) + if typeStrings: + # the directory is controlled by the project + self.setDirty(True) + self.__model.removeItem(olddn) + typeString = typeStrings[0] + del typeStrings[0] + self.__model.addNewItem(typeString, newdn, typeStrings) + else: + self.__model.renameItem(olddn, self.getAbsolutePath(newdn)) + self.directoryRemoved.emit(olddn) + + def removeFile(self, fn, updateModel=True): + """ + Public slot to remove a file from the project. + + The file is not deleted from the project directory. + + @param fn filename to be removed from the project + @param updateModel flag indicating an update of the model is + requested (boolean) + """ + fn = self.getRelativePath(fn) + dirty = True + if fn in self.pdata["SOURCES"]: + self.pdata["SOURCES"].remove(fn) + self.projectSourceRemoved.emit(fn) + elif fn in self.pdata["FORMS"]: + self.pdata["FORMS"].remove(fn) + self.projectFormRemoved.emit(fn) + elif fn in self.pdata["INTERFACES"]: + self.pdata["INTERFACES"].remove(fn) + self.projectInterfaceRemoved.emit(fn) + elif fn in self.pdata["PROTOCOLS"]: + self.pdata["PROTOCOLS"].remove(fn) + self.projectProtocolRemoved.emit(fn) + elif fn in self.pdata["RESOURCES"]: + self.pdata["RESOURCES"].remove(fn) + self.projectResourceRemoved.emit(fn) + elif fn in self.pdata["OTHERS"]: + self.pdata["OTHERS"].remove(fn) + self.projectOthersRemoved.emit(fn) + elif fn in self.pdata["TRANSLATIONS"]: + self.pdata["TRANSLATIONS"].remove(fn) + self.projectLanguageRemoved.emit(fn) + else: + dirty = False + updateModel and self.__model.removeItem(fn) + if dirty: + self.setDirty(True) + + def removeDirectory(self, dn): + """ + Public method to remove a directory from the project. + + The directory is not deleted from the project directory. + + @param dn directory name to be removed from the project + """ + dirty = False + dn = self.getRelativePath(dn) + for entry in self.pdata["OTHERS"][:]: + if entry.startswith(dn): + self.pdata["OTHERS"].remove(entry) + dirty = True + dn2 = dn if dn.endswith(os.sep) else dn + os.sep + for key in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "TRANSLATIONS", + ]: + for entry in self.pdata[key][:]: + if entry.startswith(dn2): + self.pdata[key].remove(entry) + dirty = True + self.__model.removeItem(dn) + if dirty: + self.setDirty(True) + self.directoryRemoved.emit(dn) + + def deleteFile(self, fn): + """ + Public method to delete a file from the project directory. + + @param fn filename to be deleted from the project + @return flag indicating success (boolean) + """ + try: + from send2trash import send2trash as s2t + except ImportError: + s2t = os.remove + + try: + s2t(os.path.join(self.ppath, fn)) + path, ext = os.path.splitext(fn) + if ext == ".ui": + fn2 = os.path.join(self.ppath, "{0}.h".format(fn)) + if os.path.isfile(fn2): + s2t(fn2) + head, tail = os.path.split(path) + for ext in [".pyc", ".pyo"]: + fn2 = os.path.join(self.ppath, path + ext) + if os.path.isfile(fn2): + s2t(fn2) + pat = os.path.join( + self.ppath, head, "__pycache__", "{0}.*{1}".format(tail, ext) + ) + for f in glob.glob(pat): + s2t(f) + except OSError as err: + EricMessageBox.critical( + self.ui, + self.tr("Delete file"), + self.tr( + "<p>The selected file <b>{0}</b> could not be" + " deleted.</p><p>Reason: {1}</p>" + ).format(fn, str(err)), + ) + return False + + self.removeFile(fn) + if ext == ".ui": + self.removeFile(fn + ".h") + return True + + def deleteDirectory(self, dn): + """ + Public method to delete a directory from the project directory. + + @param dn directory name to be removed from the project + @return flag indicating success (boolean) + """ + if not os.path.isabs(dn): + dn = os.path.join(self.ppath, dn) + try: + try: + from send2trash import send2trash + + send2trash(dn) + except ImportError: + shutil.rmtree(dn, True) + except OSError as err: + EricMessageBox.critical( + self.ui, + self.tr("Delete directory"), + self.tr( + "<p>The selected directory <b>{0}</b> could not be" + " deleted.</p><p>Reason: {1}</p>" + ).format(dn, str(err)), + ) + return False + + self.removeDirectory(dn) + return True + + def hasEntry(self, fn): + """ + Public method to check the project for a file. + + @param fn filename to be checked (string) + @return flag indicating, if the project contains the file (boolean) + """ + fn = self.getRelativePath(fn) + return ( + fn in self.pdata["SOURCES"] + or fn in self.pdata["FORMS"] + or fn in self.pdata["INTERFACES"] + or fn in self.pdata["PROTOCOLS"] + or fn in self.pdata["RESOURCES"] + or fn in self.pdata["OTHERS"] + ) + + def createNewProject(self): + """ + Public slot to built a new project. + + This method displays the new project dialog and initializes + the 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.closeProject() + dlg.storeData() + self.pdata["VCS"] = "None" + self.opened = True + if not self.pdata["FILETYPES"]: + self.initFileTypes() + self.setDirty(True) + self.closeAct.setEnabled(True) + self.saveasAct.setEnabled(True) + self.actGrp2.setEnabled(True) + self.propsAct.setEnabled(True) + self.userPropsAct.setEnabled(True) + self.filetypesAct.setEnabled(True) + self.lexersAct.setEnabled(True) + self.sessActGrp.setEnabled(False) + self.dbgActGrp.setEnabled(True) + self.menuDebuggerAct.setEnabled(True) + self.menuSessionAct.setEnabled(False) + self.menuCheckAct.setEnabled(True) + self.menuShowAct.setEnabled(True) + self.menuDiagramAct.setEnabled(True) + self.menuApidocAct.setEnabled(True) + self.menuPackagersAct.setEnabled(True) + self.pluginGrp.setEnabled(self.pdata["PROJECTTYPE"] in ["E7Plugin"]) + self.addLanguageAct.setEnabled(bool(self.pdata["TRANSLATIONPATTERN"])) + self.makeGrp.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"]) + self.menuMakeAct.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"]) + self.menuOtherToolsAct.setEnabled(True) + self.menuFormattingAct.setEnabled(True) + + self.projectAboutToBeCreated.emit() + + hashStr = str( + QCryptographicHash.hash( + QByteArray(self.ppath.encode("utf-8")), + QCryptographicHash.Algorithm.Sha1, + ).toHex(), + encoding="utf-8", + ) + self.pdata["HASH"] = hashStr + + if self.pdata["PROGLANGUAGE"] == "MicroPython": + # change the lexer association for *.py files + self.pdata["LEXERASSOCS"] = { + "*.py": "MicroPython", + } + + # create the project directory if it doesn't exist already + if not os.path.isdir(self.ppath): + try: + os.makedirs(self.ppath) + except OSError: + EricMessageBox.critical( + self.ui, + self.tr("Create project directory"), + self.tr( + "<p>The project directory <b>{0}</b> could not" + " be created.</p>" + ).format(self.ppath), + ) + self.vcs = self.initVCS() + return + + # create an empty __init__.py file to make it a Python package + # (only for Python and Python3) + if self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]: + fn = os.path.join(self.ppath, "__init__.py") + with open(fn, "w", encoding="utf-8"): + pass + self.appendFile(fn, True) + + # create an empty main script file, if a name was given + if self.pdata["MAINSCRIPT"]: + if not os.path.isabs(self.pdata["MAINSCRIPT"]): + ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"]) + else: + ms = self.pdata["MAINSCRIPT"] + os.makedirs(os.path.dirname(ms), exist_ok=True) + with open(ms, "w"): + pass + self.appendFile(ms, True) + + if self.pdata["MAKEPARAMS"]["MakeEnabled"]: + mf = self.pdata["MAKEPARAMS"]["MakeFile"] + if mf: + if not os.path.isabs(mf): + mf = os.path.join(self.ppath, mf) + else: + mf = os.path.join(self.ppath, Project.DefaultMakefile) + os.makedirs(os.path.dirname(mf), exist_ok=True) + with open(mf, "w"): + pass + self.appendFile(mf) + + tpd = os.path.join(self.ppath, self.translationsRoot) + if not self.translationsRoot.endswith(os.sep): + tpd = os.path.dirname(tpd) + if not os.path.isdir(tpd): + os.makedirs(tpd, exist_ok=True) + if self.pdata["TRANSLATIONSBINPATH"]: + tpd = os.path.join(self.ppath, self.pdata["TRANSLATIONSBINPATH"]) + if not os.path.isdir(tpd): + os.makedirs(tpd, exist_ok=True) + + # create management directory if not present + self.createProjectManagementDir() + + self.saveProject() + addAllToVcs = True + else: + try: + # create management directory if not present + self.createProjectManagementDir() + except OSError: + EricMessageBox.critical( + self.ui, + self.tr("Create project management directory"), + self.tr( + "<p>The project directory <b>{0}</b> is not" + " writable.</p>" + ).format(self.ppath), + ) + return + + if self.pdata["MAINSCRIPT"]: + if not os.path.isabs(self.pdata["MAINSCRIPT"]): + ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"]) + else: + ms = self.pdata["MAINSCRIPT"] + if not os.path.exists(ms): + try: + os.makedirs(os.path.dirname(ms)) + with open(ms, "w"): + pass + except OSError as err: + EricMessageBox.critical( + self.ui, + self.tr("Create main script"), + self.tr( + "<p>The mainscript <b>{0}</b> could not" + " be created.<br/>Reason: {1}</p>" + ).format(ms, str(err)), + ) + self.appendFile(ms, True) + else: + ms = "" + + if self.pdata["MAKEPARAMS"]["MakeEnabled"]: + mf = self.pdata["MAKEPARAMS"]["MakeFile"] + if mf: + if not os.path.isabs(mf): + mf = os.path.join(self.ppath, mf) + else: + mf = os.path.join(self.ppath, Project.DefaultMakefile) + if not os.path.exists(mf): + try: + os.makedirs(os.path.dirname(mf)) + with open(mf, "w"): + pass + except OSError as err: + EricMessageBox.critical( + self.ui, + self.tr("Create Makefile"), + self.tr( + "<p>The makefile <b>{0}</b> could not" + " be created.<br/>Reason: {1}</p>" + ).format(mf, str(err)), + ) + self.appendFile(mf) + + # add existing files to the project + res = EricMessageBox.yesNo( + self.ui, + self.tr("New Project"), + self.tr("""Add existing files to the project?"""), + yesDefault=True, + ) + if res: + self.newProjectAddFiles(ms) + addAllToVcs = res + # create an empty __init__.py file to make it a Python package + # if none exists (only for Python and Python3) + if self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]: + fn = os.path.join(self.ppath, "__init__.py") + if not os.path.exists(fn): + with open(fn, "w", encoding="utf-8"): + pass + self.appendFile(fn, True) + self.saveProject() + + # check, if the existing project directory is already under + # VCS control + pluginManager = ericApp().getObject("PluginManager") + for indicator, vcsData in list( + pluginManager.getVcsSystemIndicators().items() + ): + if os.path.exists(os.path.join(self.ppath, indicator)): + if len(vcsData) > 1: + vcsList = [] + for _vcsSystemStr, vcsSystemDisplay in vcsData: + vcsList.append(vcsSystemDisplay) + res, vcs_ok = QInputDialog.getItem( + None, + self.tr("New Project"), + self.tr("Select Version Control System"), + vcsList, + 0, + False, + ) + if vcs_ok: + for vcsSystemStr, vcsSystemDisplay in vcsData: + if res == vcsSystemDisplay: + vcsSystem = vcsSystemStr + break + else: + vcsSystem = "None" + else: + vcsSystem = "None" + else: + vcsSystem = vcsData[0][1] + self.pdata["VCS"] = vcsSystem + self.vcs = self.initVCS() + self.setDirty(True) + if self.vcs is not None: + # edit VCS command options + if self.vcs.vcsSupportCommandOptions(): + vcores = EricMessageBox.yesNo( + self.ui, + self.tr("New Project"), + self.tr( + """Would you like to edit the VCS""" + """ command options?""" + ), + ) + else: + vcores = False + if vcores: + from VCS.CommandOptionsDialog import ( + VcsCommandOptionsDialog, + ) + + codlg = VcsCommandOptionsDialog(self.vcs) + if codlg.exec() == QDialog.DialogCode.Accepted: + self.vcs.vcsSetOptions(codlg.getOptions()) + # add project file to repository + if res == 0: + apres = EricMessageBox.yesNo( + self.ui, + self.tr("New project"), + self.tr( + "Shall the project file be added" + " to the repository?" + ), + yesDefault=True, + ) + if apres: + self.saveProject() + self.vcs.vcsAdd(self.pfile) + else: + self.pdata["VCS"] = "None" + self.saveProject() + break + + # put the project under VCS control + if self.vcs is None and self.vcsSoftwareAvailable() and self.vcsRequested: + vcsSystemsDict = ( + ericApp() + .getObject("PluginManager") + .getPluginDisplayStrings("version_control") + ) + vcsSystemsDisplay = [self.tr("None")] + keys = sorted(vcsSystemsDict.keys()) + for key in keys: + vcsSystemsDisplay.append(vcsSystemsDict[key]) + vcsSelected, ok = QInputDialog.getItem( + None, + self.tr("New Project"), + self.tr("Select version control system for the project"), + vcsSystemsDisplay, + 0, + False, + ) + if ok and vcsSelected != self.tr("None"): + for vcsSystem, vcsSystemDisplay in vcsSystemsDict.items(): + if vcsSystemDisplay == vcsSelected: + self.pdata["VCS"] = vcsSystem + break + else: + self.pdata["VCS"] = "None" + else: + self.pdata["VCS"] = "None" + self.vcs = self.initVCS() + if self.vcs is not None: + vcsdlg = self.vcs.vcsOptionsDialog(self, self.name) + if vcsdlg.exec() == QDialog.DialogCode.Accepted: + vcsDataDict = vcsdlg.getData() + else: + self.pdata["VCS"] = "None" + self.vcs = self.initVCS() + self.setDirty(True) + if self.vcs is not None: + # edit VCS command options + if self.vcs.vcsSupportCommandOptions(): + vcores = EricMessageBox.yesNo( + self.ui, + self.tr("New Project"), + self.tr( + """Would you like to edit the VCS command""" + """ options?""" + ), + ) + else: + vcores = False + if vcores: + from VCS.CommandOptionsDialog import VcsCommandOptionsDialog + + codlg = VcsCommandOptionsDialog(self.vcs) + if codlg.exec() == QDialog.DialogCode.Accepted: + self.vcs.vcsSetOptions(codlg.getOptions()) + + # create the project in the VCS + self.vcs.vcsSetDataFromDict(vcsDataDict) + self.saveProject() + self.vcs.vcsConvertProject(vcsDataDict, self, addAll=addAllToVcs) + else: + self.newProjectHooks.emit() + self.newProject.emit() + + else: + self.newProjectHooks.emit() + self.newProject.emit() + + def newProjectAddFiles(self, mainscript): + """ + Public method to add files to a new project. + + @param mainscript name of the mainscript (string) + """ + # Show the file type associations for the user to change + self.__showFiletypeAssociations() + + with EricOverrideCursor(): + # search the project directory for files with known extensions + filespecs = list(self.pdata["FILETYPES"].keys()) + for filespec in filespecs: + files = Utilities.direntries(self.ppath, True, filespec) + for file in files: + self.appendFile(file) + + # special handling for translation files + if self.translationsRoot: + tpd = os.path.join(self.ppath, self.translationsRoot) + if not self.translationsRoot.endswith(os.sep): + tpd = os.path.dirname(tpd) + else: + tpd = self.ppath + tslist = [] + if self.pdata["TRANSLATIONPATTERN"]: + pattern = os.path.basename(self.pdata["TRANSLATIONPATTERN"]) + if "%language%" in pattern: + pattern = pattern.replace("%language%", "*") + else: + tpd = self.pdata["TRANSLATIONPATTERN"].split("%language%")[0] + else: + pattern = "*.ts" + tslist.extend(Utilities.direntries(tpd, True, pattern)) + pattern = self.__binaryTranslationFile(pattern) + if pattern: + tslist.extend(Utilities.direntries(tpd, True, pattern)) + if tslist: + if "_" in os.path.basename(tslist[0]): + # the first entry determines the mainscript name + mainscriptname = ( + os.path.splitext(mainscript)[0] + or os.path.basename(tslist[0]).split("_")[0] + ) + self.pdata["TRANSLATIONPATTERN"] = os.path.join( + os.path.dirname(tslist[0]), + "{0}_%language%{1}".format( + os.path.basename(tslist[0]).split("_")[0], + os.path.splitext(tslist[0])[1], + ), + ) + else: + mainscriptname = "" + pattern, ok = QInputDialog.getText( + None, + self.tr("Translation Pattern"), + self.tr( + "Enter the path pattern for translation files " + "(use '%language%' in place of the language" + " code):" + ), + QLineEdit.EchoMode.Normal, + tslist[0], + ) + if pattern: + self.pdata["TRANSLATIONPATTERN"] = pattern + if self.pdata["TRANSLATIONPATTERN"]: + self.pdata["TRANSLATIONPATTERN"] = self.getRelativePath( + self.pdata["TRANSLATIONPATTERN"] + ) + pattern = self.pdata["TRANSLATIONPATTERN"].replace( + "%language%", "*" + ) + for ts in tslist: + if fnmatch.fnmatch(ts, pattern): + self.pdata["TRANSLATIONS"].append(ts) + self.projectLanguageAdded.emit(ts) + if self.pdata["TRANSLATIONSBINPATH"]: + tpd = os.path.join( + self.ppath, self.pdata["TRANSLATIONSBINPATH"] + ) + pattern = os.path.basename( + self.pdata["TRANSLATIONPATTERN"] + ).replace("%language%", "*") + pattern = self.__binaryTranslationFile(pattern) + qmlist = Utilities.direntries(tpd, True, pattern) + for qm in qmlist: + self.pdata["TRANSLATIONS"].append(qm) + self.projectLanguageAdded.emit(qm) + if not self.pdata["MAINSCRIPT"] and bool(mainscriptname): + if self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"]: + self.pdata["MAINSCRIPT"] = "{0}.py".format(mainscriptname) + elif self.pdata["PROGLANGUAGE"] == "Ruby": + self.pdata["MAINSCRIPT"] = "{0}.rb".format(mainscriptname) + self.setDirty(True) + + def __showProperties(self): + """ + Private slot to display the properties dialog. + """ + from .PropertiesDialog import PropertiesDialog + + dlg = PropertiesDialog(self, False) + if dlg.exec() == QDialog.DialogCode.Accepted: + projectType = self.pdata["PROJECTTYPE"] + dlg.storeData() + self.setDirty(True) + if self.pdata["MAINSCRIPT"]: + if not os.path.isabs(self.pdata["MAINSCRIPT"]): + ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"]) + else: + ms = self.pdata["MAINSCRIPT"] + if os.path.exists(ms): + self.appendFile(ms) + + if self.pdata["MAKEPARAMS"]["MakeEnabled"]: + mf = self.pdata["MAKEPARAMS"]["MakeFile"] + if mf: + if not os.path.isabs(mf): + mf = os.path.join(self.ppath, mf) + else: + mf = os.path.join(self.ppath, Project.DefaultMakefile) + if not os.path.exists(mf): + try: + with open(mf, "w"): + pass + except OSError as err: + EricMessageBox.critical( + self.ui, + self.tr("Create Makefile"), + self.tr( + "<p>The makefile <b>{0}</b> could not" + " be created.<br/>Reason: {1}</p>" + ).format(mf, str(err)), + ) + self.appendFile(mf) + + if self.pdata["PROJECTTYPE"] != projectType: + # reinitialize filetype associations + self.initFileTypes() + + if self.translationsRoot: + tp = os.path.join(self.ppath, self.translationsRoot) + if not self.translationsRoot.endswith(os.sep): + tp = os.path.dirname(tp) + else: + tp = self.ppath + if not os.path.isdir(tp): + os.makedirs(tp) + if tp != self.ppath and tp not in self.subdirs: + self.subdirs.append(tp) + + if self.pdata["TRANSLATIONSBINPATH"]: + tp = os.path.join(self.ppath, self.pdata["TRANSLATIONSBINPATH"]) + if not os.path.isdir(tp): + os.makedirs(tp) + if tp != self.ppath and tp not in self.subdirs: + self.subdirs.append(tp) + + self.pluginGrp.setEnabled(self.pdata["PROJECTTYPE"] in ["E7Plugin"]) + + self.__model.projectPropertiesChanged() + self.projectPropertiesChanged.emit() + + if self.pdata["PROJECTTYPE"] != projectType: + self.__reorganizeFiles() + + def __showUserProperties(self): + """ + Private slot to display the user specific properties dialog. + """ + vcsSystem = self.pdata["VCS"] or None + vcsSystemOverride = self.pudata["VCSOVERRIDE"] or None + + from .UserPropertiesDialog import UserPropertiesDialog + + dlg = UserPropertiesDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + dlg.storeData() + + if ( + (self.pdata["VCS"] and self.pdata["VCS"] != vcsSystem) + or ( + self.pudata["VCSOVERRIDE"] + and self.pudata["VCSOVERRIDE"] != vcsSystemOverride + ) + or (vcsSystemOverride is not None and not self.pudata["VCSOVERRIDE"]) + ): + # stop the VCS monitor thread and shutdown VCS + if self.vcs is not None: + self.vcs.stopStatusMonitor() + self.vcs.vcsShutdown() + self.vcs.deleteLater() + self.vcs = None + ericApp().getObject("PluginManager").deactivateVcsPlugins() + # reinit VCS + self.vcs = self.initVCS() + # start the VCS monitor thread + self.__vcsConnectStatusMonitor() + self.reinitVCS.emit() + + if self.pudata["VCSSTATUSMONITORINTERVAL"]: + self.setStatusMonitorInterval(self.pudata["VCSSTATUSMONITORINTERVAL"]) + else: + self.setStatusMonitorInterval( + Preferences.getVCS("StatusMonitorInterval") + ) + + def __showFiletypeAssociations(self): + """ + Private slot to display the filetype association dialog. + """ + from .FiletypeAssociationDialog import FiletypeAssociationDialog + + dlg = FiletypeAssociationDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + dlg.transferData() + self.setDirty(True) + self.__reorganizeFiles() + + def getFiletypeAssociations(self, associationType): + """ + Public method to get the list of file type associations for + the given association type. + + @param associationType type of the association (one of FORMS, + INTERFACES, OTHERS, PROTOCOLS, RESOURCES, SOURCES, + TRANSLATIONS or __IGNORE__) + @type str + @return list of file patterns for the given type + @rtype list of str + """ + return [ + assoc + for assoc in self.pdata["FILETYPES"] + if self.pdata["FILETYPES"][assoc] == associationType + ] + + def __showLexerAssociations(self): + """ + Private slot to display the lexer association dialog. + """ + from .LexerAssociationDialog import LexerAssociationDialog + + dlg = LexerAssociationDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + dlg.transferData() + self.setDirty(True) + self.lexerAssociationsChanged.emit() + + def getEditorLexerAssoc(self, filename): + """ + Public method to retrieve a lexer association. + + @param filename filename used to determine the associated lexer + language (string) + @return the requested lexer language (string) + """ + # try user settings first + for pattern, language in list(self.pdata["LEXERASSOCS"].items()): + if fnmatch.fnmatch(filename, pattern): + return language + + # try project type specific defaults next + projectType = self.pdata["PROJECTTYPE"] + with contextlib.suppress(KeyError): + if self.__lexerAssociationCallbacks[projectType] is not None: + return self.__lexerAssociationCallbacks[projectType](filename) + + # return empty string to signal to use the global setting + return "" + + def getIgnorePatterns(self): + """ + Public method to get the list of file name patterns for files to be + ignored. + + @return list of ignore file name patterns + @rtype list of str + """ + return self.getFiletypeAssociations("__IGNORE__") + + @pyqtSlot() + @pyqtSlot(str) + def openProject(self, fn=None, restoreSession=True, reopen=False): + """ + Public slot to open a project. + + @param fn optional filename of the project file to be read + @param restoreSession flag indicating to restore the project + session (boolean) + @param reopen flag indicating a reopening of the project (boolean) + """ + if not self.checkDirty(): + return + + if fn is None: + fn = EricFileDialog.getOpenFileName( + self.parent(), + self.tr("Open project"), + Preferences.getMultiProject("Workspace") or Utilities.getHomeDir(), + self.tr("Project Files (*.epj);;XML Project Files (*.e4p)"), + ) + + if fn and self.closeProject(): + with EricOverrideCursor(): + ok = self.__readProject(fn) + if ok: + self.opened = True + if not self.pdata["FILETYPES"]: + self.initFileTypes() + else: + self.updateFileTypes() + + try: + # create management directory if not present + self.createProjectManagementDir() + except OSError: + EricMessageBox.critical( + self.ui, + self.tr("Create project management directory"), + self.tr( + "<p>The project directory <b>{0}</b> is not" + " writable.</p>" + ).format(self.ppath), + ) + return + + # read a user specific project file + self.__readUserProperties() + + with EricOverrideCursor(): + oldState = self.isDirty() + self.vcs = self.initVCS() + if self.vcs is None and self.isDirty() == oldState: + # check, if project is version controlled + pluginManager = ericApp().getObject("PluginManager") + for ( + indicator, + vcsData, + ) in pluginManager.getVcsSystemIndicators().items(): + if os.path.exists(os.path.join(self.ppath, indicator)): + if len(vcsData) > 1: + vcsList = [] + for (_vcsSystemStr, vcsSystemDisplay) in vcsData: + vcsList.append(vcsSystemDisplay) + with EricOverridenCursor(): + res, vcs_ok = QInputDialog.getItem( + None, + self.tr("New Project"), + self.tr("Select Version Control" " System"), + vcsList, + 0, + False, + ) + if vcs_ok: + for (vcsSystemStr, vcsSystemDisplay) in vcsData: + if res == vcsSystemDisplay: + vcsSystem = vcsSystemStr + break + else: + vcsSystem = "None" + else: + vcsSystem = "None" + else: + vcsSystem = vcsData[0][0] + self.pdata["VCS"] = vcsSystem + self.vcs = self.initVCS() + self.setDirty(True) + if self.vcs is not None and ( + self.vcs.vcsRegisteredState(self.ppath) + != self.vcs.canBeCommitted + ): + self.pdata["VCS"] = "None" + self.vcs = self.initVCS() + self.closeAct.setEnabled(True) + self.saveasAct.setEnabled(True) + self.actGrp2.setEnabled(True) + self.propsAct.setEnabled(True) + self.userPropsAct.setEnabled(True) + self.filetypesAct.setEnabled(True) + self.lexersAct.setEnabled(True) + self.sessActGrp.setEnabled(True) + self.dbgActGrp.setEnabled(True) + self.menuDebuggerAct.setEnabled(True) + self.menuSessionAct.setEnabled(True) + self.menuCheckAct.setEnabled(True) + self.menuShowAct.setEnabled(True) + self.menuDiagramAct.setEnabled(True) + self.menuApidocAct.setEnabled(True) + self.menuPackagersAct.setEnabled(True) + self.pluginGrp.setEnabled(self.pdata["PROJECTTYPE"] in ["E7Plugin"]) + self.addLanguageAct.setEnabled( + bool(self.pdata["TRANSLATIONPATTERN"]) + ) + self.makeGrp.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"]) + self.menuMakeAct.setEnabled(self.pdata["MAKEPARAMS"]["MakeEnabled"]) + self.menuOtherToolsAct.setEnabled(True) + self.menuFormattingAct.setEnabled(True) + + # open a project debugger properties file being quiet + # about errors + if Preferences.getProject("AutoLoadDbgProperties"): + self.__readDebugProperties(True) + + self.__model.projectOpened() + self.projectOpenedHooks.emit() + self.projectOpened.emit() + + if Preferences.getProject("SearchNewFiles"): + self.__doSearchNewFiles() + + # read a project tasks file + self.__readTasks() + self.ui.taskViewer.setProjectOpen(True) + # rescan project tasks + if Preferences.getProject("TasksProjectRescanOnOpen"): + ericApp().getObject("TaskViewer").regenerateProjectTasks(quiet=True) + + if restoreSession: + # open the main script + if self.pdata["MAINSCRIPT"]: + if not os.path.isabs(self.pdata["MAINSCRIPT"]): + ms = os.path.join(self.ppath, self.pdata["MAINSCRIPT"]) + else: + ms = self.pdata["MAINSCRIPT"] + self.sourceFile.emit(ms) + + # open a project session file being quiet about errors + if reopen: + self.__readSession(quiet=True, indicator="_tmp") + elif Preferences.getProject("AutoLoadSession"): + self.__readSession(quiet=True) + + # start the VCS monitor thread + self.__vcsConnectStatusMonitor() + + def reopenProject(self): + """ + Public slot to reopen the current project. + """ + projectFile = self.pfile + res = self.closeProject(reopen=True) + if res: + self.openProject(projectFile, reopen=True) + + def saveProject(self): + """ + Public slot to save the current project. + + @return flag indicating success + """ + if self.isDirty(): + if len(self.pfile) > 0: + if self.pfile.endswith(".e4p"): + self.pfile = self.pfile.replace(".e4p", ".epj") + self.__syncRecent() + ok = self.__writeProject() + else: + ok = self.saveProjectAs() + else: + ok = True + self.sessActGrp.setEnabled(ok) + self.menuSessionAct.setEnabled(ok) + return ok + + def saveProjectAs(self): + """ + Public slot to save the current project to a different file. + + @return flag indicating success (boolean) + """ + defaultFilter = self.tr("Project Files (*.epj)") + defaultPath = ( + self.ppath + if self.ppath + else (Preferences.getMultiProject("Workspace") or Utilities.getHomeDir()) + ) + fn, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self.parent(), + self.tr("Save Project"), + defaultPath, + self.tr("Project Files (*.epj)"), + defaultFilter, + EricFileDialog.DontConfirmOverwrite, + ) + + if fn: + fpath = pathlib.Path(fn) + if not fpath.suffix: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fpath = fpath.with_suffix(ex) + if fpath.exists(): + res = EricMessageBox.yesNo( + self.ui, + self.tr("Save File"), + self.tr( + """<p>The file <b>{0}</b> already exists.""" + """ Overwrite it?</p>""" + ).format(fpath), + icon=EricMessageBox.Warning, + ) + if not res: + return False + + self.name = fpath.stem + ok = self.__writeProject(str(fpath)) + + if ok: + # create management directory if not present + self.createProjectManagementDir() + + # now save the tasks + self.writeTasks() + + self.sessActGrp.setEnabled(ok) + self.menuSessionAct.setEnabled(ok) + self.projectClosedHooks.emit() + self.projectClosed.emit(False) + self.projectOpenedHooks.emit() + self.projectOpened.emit() + return ok + else: + return False + + def checkDirty(self): + """ + Public method to check dirty status and open a message window. + + @return flag indicating whether this operation was successful (boolean) + """ + if self.isDirty(): + res = EricMessageBox.okToClearData( + self.parent(), + self.tr("Close Project"), + self.tr("The current project has unsaved changes."), + self.saveProject, + ) + if res: + self.setDirty(False) + return res + + return True + + def __closeAllWindows(self): + """ + Private method to close all project related windows. + """ + self.codemetrics and self.codemetrics.close() + self.codecoverage and self.codecoverage.close() + self.profiledata and self.profiledata.close() + self.applicationDiagram and self.applicationDiagram.close() + self.loadedDiagram and self.loadedDiagram.close() + + @pyqtSlot() + def closeProject(self, reopen=False, noSave=False, shutdown=False): + """ + Public slot to close the current project. + + @param reopen flag indicating a reopening of the project + @type bool + @param noSave flag indicating to not perform save actions + @type bool + @param shutdown flag indicating the IDE shutdown + @type bool + @return flag indicating success + @rtype bool + """ + # save the list of recently opened projects + self.__saveRecent() + + if not self.isOpen(): + return True + + if not self.checkDirty(): + return False + + ericApp().getObject("TaskViewer").stopProjectTaskExtraction() + + # save the user project properties + if not noSave: + self.__writeUserProperties() + + # save the project session file being quiet about error + if reopen: + self.__writeSession(quiet=True, indicator="_tmp") + elif Preferences.getProject("AutoSaveSession") and not noSave: + self.__writeSession(quiet=True) + + # save the project debugger properties file being quiet about error + if ( + Preferences.getProject("AutoSaveDbgProperties") + and self.isDebugPropertiesLoaded() + and not noSave + and self.debugPropertiesChanged + ): + self.__writeDebugProperties(True) + + vm = ericApp().getObject("ViewManager") + + # check dirty status of all project files first + for fn in vm.getOpenFilenames(): + if self.isProjectFile(fn): + reset = vm.checkFileDirty(fn) + if not reset: + # abort shutting down + return False + + # close all project related editors + success = True + for fn in vm.getOpenFilenames(): + if self.isProjectFile(fn): + success &= vm.closeWindow(fn, ignoreDirty=True) + if not success: + return False + + # stop the VCS monitor thread + if self.vcs is not None: + self.vcs.stopStatusMonitor() + + # now save the tasks + if not noSave: + self.writeTasks() + self.ui.taskViewer.clearProjectTasks() + self.ui.taskViewer.setProjectOpen(False) + + # now shutdown the vcs interface + if self.vcs: + self.vcs.vcsShutdown() + self.vcs.deleteLater() + self.vcs = None + ericApp().getObject("PluginManager").deactivateVcsPlugins() + + # now close all project related tool windows + self.__closeAllWindows() + + self.__initData() + self.closeAct.setEnabled(False) + self.saveasAct.setEnabled(False) + self.saveAct.setEnabled(False) + self.actGrp2.setEnabled(False) + self.propsAct.setEnabled(False) + self.userPropsAct.setEnabled(False) + self.filetypesAct.setEnabled(False) + self.lexersAct.setEnabled(False) + self.sessActGrp.setEnabled(False) + self.dbgActGrp.setEnabled(False) + self.menuDebuggerAct.setEnabled(False) + self.menuSessionAct.setEnabled(False) + self.menuCheckAct.setEnabled(False) + self.menuShowAct.setEnabled(False) + self.menuDiagramAct.setEnabled(False) + self.menuApidocAct.setEnabled(False) + self.menuPackagersAct.setEnabled(False) + self.pluginGrp.setEnabled(False) + self.makeGrp.setEnabled(False) + self.menuMakeAct.setEnabled(False) + self.menuOtherToolsAct.setEnabled(False) + self.menuFormattingAct.setEnabled(False) + + self.__model.projectClosed() + self.projectClosedHooks.emit() + self.projectClosed.emit(shutdown) + + return True + + def saveAllScripts(self, reportSyntaxErrors=False): + """ + Public method to save all scripts belonging to the project. + + @param reportSyntaxErrors flag indicating special reporting + for syntax errors (boolean) + @return flag indicating success (boolean) + """ + vm = ericApp().getObject("ViewManager") + success = True + filesWithSyntaxErrors = 0 + for fn in vm.getOpenFilenames(): + rfn = self.getRelativePath(fn) + if rfn in self.pdata["SOURCES"] or rfn in self.pdata["OTHERS"]: + editor = vm.getOpenEditor(fn) + success &= vm.saveEditorEd(editor) + if reportSyntaxErrors and editor.hasSyntaxErrors(): + filesWithSyntaxErrors += 1 + + if reportSyntaxErrors and filesWithSyntaxErrors > 0: + EricMessageBox.critical( + self.ui, + self.tr("Syntax errors detected"), + self.tr( + """The project contains %n file(s) with syntax errors.""", + "", + filesWithSyntaxErrors, + ), + ) + return False + else: + return success + + def checkAllScriptsDirty(self, reportSyntaxErrors=False): + """ + Public method to check all scripts belonging to the project for + their dirty status. + + @param reportSyntaxErrors flag indicating special reporting + for syntax errors (boolean) + @return flag indicating success (boolean) + """ + vm = ericApp().getObject("ViewManager") + success = True + filesWithSyntaxErrors = 0 + for fn in vm.getOpenFilenames(): + rfn = self.getRelativePath(fn) + if rfn in self.pdata["SOURCES"] or rfn in self.pdata["OTHERS"]: + editor = vm.getOpenEditor(fn) + success &= editor.checkDirty() + if reportSyntaxErrors and editor.hasSyntaxErrors(): + filesWithSyntaxErrors += 1 + + if reportSyntaxErrors and filesWithSyntaxErrors > 0: + EricMessageBox.critical( + self.ui, + self.tr("Syntax errors detected"), + self.tr( + """The project contains %n file(s) with syntax errors.""", + "", + filesWithSyntaxErrors, + ), + ) + return False + else: + return success + + def getMainScript(self, normalized=False): + """ + Public method to return the main script filename. + + The normalized name is the name of the main script prepended with + the project path. + + @param normalized flag indicating a normalized filename is wanted + @type bool + @return filename of the projects main script + @rtype str + """ + if self.pdata["MAINSCRIPT"]: + if normalized: + return os.path.join(self.ppath, self.pdata["MAINSCRIPT"]) + else: + return self.pdata["MAINSCRIPT"] + else: + return "" + + def getSources(self, normalized=False): + """ + Public method to return the source script files. + + @param normalized flag indicating a normalized filename is wanted + @type bool + @return list of the projects scripts + @rtype list of str + """ + return self.getProjectFiles("SOURCES", normalized=normalized) + + def getProjectFiles(self, fileType, normalized=False): + """ + Public method to get the file entries of the given type. + + @param fileType project file type (one of SOURCES, FORMS, RESOURCES, + INTERFACES, PROTOCOLS, OTHERS, TRANSLATIONS) + @type str + @param normalized flag indicating normalized file names are wanted + @type boolean + @return list of file names + @rtype list of str + @exception ValueError raised when an unsupported file type is given + """ + if fileType not in [ + "SOURCES", + "FORMS", + "RESOURCES", + "INTERFACES", + "PROTOCOLS", + "OTHERS", + "TRANSLATIONS", + ]: + raise ValueError("Given file type has incorrect value.") + + if normalized: + return [os.path.join(self.ppath, fn) for fn in self.pdata[fileType]] + else: + return self.pdata[fileType] + + def getProjectType(self): + """ + Public method to get the type of the project. + + @return UI type of the project (string) + """ + return self.pdata["PROJECTTYPE"] + + def getProjectLanguage(self): + """ + Public method to get the project's programming language. + + @return programming language (string) + """ + return self.pdata["PROGLANGUAGE"] + + def isMixedLanguageProject(self): + """ + Public method to check, if this is a mixed language project. + + @return flag indicating a mixed language project + @rtype bool + """ + return self.pdata["MIXEDLANGUAGE"] + + def isPythonProject(self): + """ + Public method to check, if this project is a Python3 or MicroPython + project. + + @return flag indicating a Python project (boolean) + """ + return self.pdata["PROGLANGUAGE"] in ["Python3", "MicroPython"] + + def isPy3Project(self): + """ + Public method to check, if this project is a Python3 project. + + @return flag indicating a Python3 project (boolean) + """ + return self.pdata["PROGLANGUAGE"] == "Python3" + + def isMicroPythonProject(self): + """ + Public method to check, if this project is a MicroPython project. + + @return flag indicating a MicroPython project + @rtype bool + """ + return self.pdata["PROGLANGUAGE"] == "MicroPython" + + def isRubyProject(self): + """ + Public method to check, if this project is a Ruby project. + + @return flag indicating a Ruby project (boolean) + """ + return self.pdata["PROGLANGUAGE"] == "Ruby" + + def isJavaScriptProject(self): + """ + Public method to check, if this project is a JavaScript project. + + @return flag indicating a JavaScript project (boolean) + """ + return self.pdata["PROGLANGUAGE"] == "JavaScript" + + def getProjectSpellLanguage(self): + """ + Public method to get the project's programming language. + + @return programming language (string) + """ + return self.pdata["SPELLLANGUAGE"] + + def getProjectDictionaries(self): + """ + Public method to get the names of the project specific dictionaries. + + @return tuple of two strings giving the absolute path names of the + project specific word and exclude list + """ + pwl = "" + if self.pdata["SPELLWORDS"]: + pwl = os.path.join(self.ppath, self.pdata["SPELLWORDS"]) + if not os.path.isfile(pwl): + pwl = "" + + pel = "" + if self.pdata["SPELLEXCLUDES"]: + pel = os.path.join(self.ppath, self.pdata["SPELLEXCLUDES"]) + if not os.path.isfile(pel): + pel = "" + + return (pwl, pel) + + def getDefaultSourceExtension(self): + """ + Public method to get the default extension for the project's + programming language. + + @return default extension (including the dot) (string) + """ + lang = self.pdata["PROGLANGUAGE"] + if lang in ("", "Python"): + lang = "Python3" + return self.__sourceExtensions(lang)[0] + + def getProjectPath(self): + """ + Public method to get the project path. + + @return project path (string) + """ + return self.ppath + + def startswithProjectPath(self, path): + """ + Public method to check, if a path starts with the project path. + + @param path path to be checked + @type str + @return flag indicating that the path starts with the project path + @rtype bool + """ + return bool(self.ppath) and ( + path == self.ppath + or Utilities.normcasepath(Utilities.toNativeSeparators(path)).startswith( + Utilities.normcasepath(Utilities.toNativeSeparators(self.ppath + "/")) + ) + ) + + def getProjectFile(self): + """ + Public method to get the path of the project file. + + @return path of the project file (string) + """ + return self.pfile + + def getProjectName(self): + """ + Public method to get the name of the project. + + The project name is determined from the name of the project file. + + @return name of the project (string) + """ + if self.pfile: + name = os.path.splitext(self.pfile)[0] + return os.path.basename(name) + else: + return "" + + def getProjectManagementDir(self): + """ + Public method to get the path of the management directory. + + @return path of the management directory (string) + """ + return os.path.join(self.ppath, ".eric7project") + + def createProjectManagementDir(self): + """ + Public method to create the project management directory. + + It does nothing, if it already exists. + """ + # create management directory if not present + mgmtDir = self.getProjectManagementDir() + if not os.path.exists(mgmtDir): + os.makedirs(mgmtDir) + + def getHash(self): + """ + Public method to get the project hash. + + @return project hash as a hex string (string) + """ + return self.pdata["HASH"] + + def getRelativePath(self, path): + """ + Public method to convert a file path to a project relative + file path. + + @param path file or directory name to convert (string) + @return project relative path or unchanged path, if path doesn't + belong to the project (string) + """ + try: + return str(pathlib.Path(path).relative_to(self.ppath)) + except ValueError: + return path + + def getRelativeUniversalPath(self, path): + """ + Public method to convert a file path to a project relative + file path with universal separators. + + @param path file or directory name to convert (string) + @return project relative path or unchanged path, if path doesn't + belong to the project (string) + """ + return Utilities.fromNativeSeparators(self.getRelativePath(path)) + + def getAbsolutePath(self, fn): + """ + Public method to convert a project relative file path to an absolute + file path. + + @param fn file or directory name to convert (string) + @return absolute path (string) + """ + if not os.path.isabs(fn): + fn = os.path.join(self.ppath, fn) + return fn + + def getAbsoluteUniversalPath(self, fn): + """ + Public method to convert a project relative file path with universal + separators to an absolute file path. + + @param fn file or directory name to convert (string) + @return absolute path (string) + """ + if not os.path.isabs(fn): + fn = os.path.join(self.ppath, Utilities.toNativeSeparators(fn)) + return fn + + def getEolString(self): + """ + Public method to get the EOL-string to be used by the project. + + @return eol string (string) + """ + if self.pdata["EOL"] >= 0: + return self.eols[self.pdata["EOL"]] + else: + eolMode = Preferences.getEditor("EOLMode") + if eolMode == QsciScintilla.EolMode.EolWindows: + eol = "\r\n" + elif eolMode == QsciScintilla.EolMode.EolUnix: + eol = "\n" + elif eolMode == QsciScintilla.EolMode.EolMac: + eol = "\r" + else: + eol = os.linesep + return eol + + def useSystemEol(self): + """ + Public method to check, if the project uses the system eol setting. + + @return flag indicating the usage of system eol (boolean) + """ + return self.pdata["EOL"] == 0 + + def getProjectVersion(self): + """ + Public mehod to get the version number of the project. + + @return version number + @rtype str + """ + return self.pdata["VERSION"] + + def getProjectAuthor(self): + """ + Public method to get the author of the project. + + @return author name + @rtype str + """ + return self.pdata["AUTHOR"] + + def getProjectAuthorEmail(self): + """ + Public method to get the email address of the project author. + + @return project author email + @rtype str + """ + return self.pdata["EMAIL"] + + def getProjectDescription(self): + """ + Public method to get the description of the project. + + @return project description + @rtype str + """ + return self.pdata["DESCRIPTION"] + + def getProjectVenv(self, resolveDebugger=True): + """ + Public method to get the name of the virtual environment used by the + project. + + @param resolveDebugger flag indicating to resolve the virtual + environment name via the debugger settings if none was configured + @type bool + @return name of the project's virtual environment + @rtype str + """ + venvName = self.getDebugProperty("VIRTUALENV") + if ( + not venvName + and resolveDebugger + and self.getProjectLanguage() in ("Python3", "MicroPython", "Cython") + ): + venvName = Preferences.getDebugger("Python3VirtualEnv") + + return venvName + + def getProjectInterpreter(self, resolveGlobal=True): + """ + Public method to get the path of the interpreter used by the project. + + @param resolveGlobal flag indicating to resolve the interpreter using + the global interpreter if no project of debugger specific + environment was configured + @type bool + @return path of the project's interpreter + @rtype str + """ + interpreter = "" + venvName = self.getProjectVenv() + if venvName: + interpreter = ( + ericApp() + .getObject("VirtualEnvManager") + .getVirtualenvInterpreter(venvName) + ) + if not interpreter and resolveGlobal: + interpreter = Globals.getPythonExecutable() + + return interpreter + + def getProjectExecPath(self): + """ + Public method to get the executable search path prefix of the project. + + @return executable search path prefix + @rtype str + """ + execPath = "" + venvName = self.getProjectVenv() + if venvName: + execPath = ( + ericApp().getObject("VirtualEnvManager").getVirtualenvExecPath(venvName) + ) + + return execPath + + def getProjectTestingFramework(self): + """ + Public method to get the testing framework name of the project. + + @return testing framework name of the project + @rtype str + """ + try: + return self.pdata["TESTING_FRAMEWORK"] + except KeyError: + return "" + + def getProjectLicense(self): + """ + Public method to get the license type used by the project. + + @return license type of the project + @rtype str + """ + try: + return self.pdata["LICENSE"] + except KeyError: + return "" + + def __isInPdata(self, fn): + """ + Private method used to check, if the passed in filename is project + controlled.. + + @param fn filename to be checked + @type str + @return flag indicating membership + @rtype bool + """ + newfn = os.path.abspath(fn) + newfn = self.getRelativePath(newfn) + return any( + newfn in self.pdata[group] + for group in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "TRANSLATIONS", + "OTHERS", + ] + ) + + def isProjectFile(self, fn): + """ + Public method used to check, if the passed in filename belongs to the + project. + + @param fn filename to be checked (string) + @return flag indicating membership (boolean) + """ + return any( + self.__checkProjectFileGroup(fn, group) + for group in [ + "SOURCES", + "FORMS", + "INTERFACES", + "PROTOCOLS", + "RESOURCES", + "TRANSLATIONS", + "OTHERS", + ] + ) + + def __checkProjectFileGroup(self, fn, group): + """ + Private method to check, if a file is in a specific file group of the + project. + + @param fn filename to be checked (string) + @param group group to check (string) + @return flag indicating membership (boolean) + """ + newfn = os.path.abspath(fn) + newfn = self.getRelativePath(newfn) + if newfn in self.pdata[group] or ( + group == "OTHERS" + and any(newfn.startswith(entry) for entry in self.pdata[group]) + ): + return True + + if Utilities.isWindowsPlatform(): + # try the above case-insensitive + newfn = newfn.lower() + if any(entry.lower() == newfn for entry in self.pdata[group]): + return True + elif group == "OTHERS" and any( + newfn.startswith(entry.lower()) for entry in self.pdata[group] + ): + return True + + return False + + def isProjectSource(self, fn): + """ + Public method used to check, if the passed in filename belongs to the + project sources. + + @param fn filename to be checked (string) + @return flag indicating membership (boolean) + """ + return self.__checkProjectFileGroup(fn, "SOURCES") + + def isProjectForm(self, fn): + """ + Public method used to check, if the passed in filename belongs to the + project forms. + + @param fn filename to be checked (string) + @return flag indicating membership (boolean) + """ + return self.__checkProjectFileGroup(fn, "FORMS") + + def isProjectInterface(self, fn): + """ + Public method used to check, if the passed in filename belongs to the + project interfaces. + + @param fn filename to be checked (string) + @return flag indicating membership (boolean) + """ + return self.__checkProjectFileGroup(fn, "INTERFACES") + + def isProjectProtocol(self, fn): + """ + Public method used to check, if the passed in filename belongs to the + project protocols. + + @param fn filename to be checked + @type str + @return flag indicating membership + @rtype bool + """ + return self.__checkProjectFileGroup(fn, "PROTOCOLS") + + def isProjectResource(self, fn): + """ + Public method used to check, if the passed in filename belongs to the + project resources. + + @param fn filename to be checked (string) + @return flag indicating membership (boolean) + """ + return self.__checkProjectFileGroup(fn, "RESOURCES") + + def initActions(self): + """ + Public slot to initialize the project related actions. + """ + self.actions = [] + + ################################################################### + ## Project actions + ################################################################### + + self.actGrp1 = createActionGroup(self) + + act = EricAction( + self.tr("New project"), + UI.PixmapCache.getIcon("projectNew"), + self.tr("&New..."), + 0, + 0, + self.actGrp1, + "project_new", + ) + act.setStatusTip(self.tr("Generate a new project")) + act.setWhatsThis( + self.tr( + """<b>New...</b>""" + """<p>This opens a dialog for entering the info for a""" + """ new project.</p>""" + ) + ) + act.triggered.connect(self.createNewProject) + self.actions.append(act) + + act = EricAction( + self.tr("Open project"), + UI.PixmapCache.getIcon("projectOpen"), + self.tr("&Open..."), + 0, + 0, + self.actGrp1, + "project_open", + ) + act.setStatusTip(self.tr("Open an existing project")) + act.setWhatsThis( + self.tr("""<b>Open...</b>""" """<p>This opens an existing project.</p>""") + ) + act.triggered.connect(self.openProject) + self.actions.append(act) + + self.closeAct = EricAction( + self.tr("Close project"), + UI.PixmapCache.getIcon("projectClose"), + self.tr("&Close"), + 0, + 0, + self, + "project_close", + ) + self.closeAct.setStatusTip(self.tr("Close the current project")) + self.closeAct.setWhatsThis( + self.tr("""<b>Close</b>""" """<p>This closes the current project.</p>""") + ) + self.closeAct.triggered.connect(self.closeProject) + self.actions.append(self.closeAct) + + self.saveAct = EricAction( + self.tr("Save project"), + UI.PixmapCache.getIcon("projectSave"), + self.tr("&Save"), + 0, + 0, + self, + "project_save", + ) + self.saveAct.setStatusTip(self.tr("Save the current project")) + self.saveAct.setWhatsThis( + self.tr("""<b>Save</b>""" """<p>This saves the current project.</p>""") + ) + self.saveAct.triggered.connect(self.saveProject) + self.actions.append(self.saveAct) + + self.saveasAct = EricAction( + self.tr("Save project as"), + UI.PixmapCache.getIcon("projectSaveAs"), + self.tr("Save &as..."), + 0, + 0, + self, + "project_save_as", + ) + self.saveasAct.setStatusTip(self.tr("Save the current project to a new file")) + self.saveasAct.setWhatsThis( + self.tr( + """<b>Save as</b>""" + """<p>This saves the current project to a new file.</p>""" + ) + ) + self.saveasAct.triggered.connect(self.saveProjectAs) + self.actions.append(self.saveasAct) + + ################################################################### + ## Project management actions + ################################################################### + + self.actGrp2 = createActionGroup(self) + + self.addFilesAct = EricAction( + self.tr("Add files to project"), + UI.PixmapCache.getIcon("fileMisc"), + self.tr("Add &files..."), + 0, + 0, + self.actGrp2, + "project_add_file", + ) + self.addFilesAct.setStatusTip(self.tr("Add files to the current project")) + self.addFilesAct.setWhatsThis( + self.tr( + """<b>Add files...</b>""" + """<p>This opens a dialog for adding files""" + """ to the current project. The place to add is""" + """ determined by the file extension.</p>""" + ) + ) + self.addFilesAct.triggered.connect(self.addFiles) + self.actions.append(self.addFilesAct) + + self.addDirectoryAct = EricAction( + self.tr("Add directory to project"), + UI.PixmapCache.getIcon("dirOpen"), + self.tr("Add directory..."), + 0, + 0, + self.actGrp2, + "project_add_directory", + ) + self.addDirectoryAct.setStatusTip( + self.tr("Add a directory to the current project") + ) + self.addDirectoryAct.setWhatsThis( + self.tr( + """<b>Add directory...</b>""" + """<p>This opens a dialog for adding a directory""" + """ to the current project.</p>""" + ) + ) + self.addDirectoryAct.triggered.connect(self.addDirectory) + self.actions.append(self.addDirectoryAct) + + self.addLanguageAct = EricAction( + self.tr("Add translation to project"), + UI.PixmapCache.getIcon("linguist4"), + self.tr("Add &translation..."), + 0, + 0, + self.actGrp2, + "project_add_translation", + ) + self.addLanguageAct.setStatusTip( + self.tr("Add a translation to the current project") + ) + self.addLanguageAct.setWhatsThis( + self.tr( + """<b>Add translation...</b>""" + """<p>This opens a dialog for add a translation""" + """ to the current project.</p>""" + ) + ) + self.addLanguageAct.triggered.connect(self.addLanguage) + self.actions.append(self.addLanguageAct) + + act = EricAction( + self.tr("Search new files"), + self.tr("Searc&h new files..."), + 0, + 0, + self.actGrp2, + "project_search_new_files", + ) + act.setStatusTip(self.tr("Search new files in the project directory.")) + act.setWhatsThis( + self.tr( + """<b>Search new files...</b>""" + """<p>This searches for new files (sources, *.ui, *.idl,""" + """ *.proto) in the project directory and registered""" + """ subdirectories.</p>""" + ) + ) + act.triggered.connect(self.__searchNewFiles) + self.actions.append(act) + + act = EricAction( + self.tr("Search Project File"), + self.tr("Search Project File..."), + QKeySequence(self.tr("Alt+Ctrl+P", "Project|Search Project File")), + 0, + self.actGrp2, + "project_search_project_file", + ) + act.setStatusTip(self.tr("Search for a file in the project list of files.")) + act.setWhatsThis( + self.tr( + """<b>Search Project File</b>""" + """<p>This searches for a file in the project list of files.</p>""" + ) + ) + act.triggered.connect(self.__searchProjectFile) + self.actions.append(act) + + self.propsAct = EricAction( + self.tr("Project properties"), + UI.PixmapCache.getIcon("projectProps"), + self.tr("&Properties..."), + 0, + 0, + self, + "project_properties", + ) + self.propsAct.setStatusTip(self.tr("Show the project properties")) + self.propsAct.setWhatsThis( + self.tr( + """<b>Properties...</b>""" + """<p>This shows a dialog to edit the project properties.</p>""" + ) + ) + self.propsAct.triggered.connect(self.__showProperties) + self.actions.append(self.propsAct) + + self.userPropsAct = EricAction( + self.tr("User project properties"), + UI.PixmapCache.getIcon("projectUserProps"), + self.tr("&User Properties..."), + 0, + 0, + self, + "project_user_properties", + ) + self.userPropsAct.setStatusTip( + self.tr("Show the user specific project properties") + ) + self.userPropsAct.setWhatsThis( + self.tr( + """<b>User Properties...</b>""" + """<p>This shows a dialog to edit the user specific project""" + """ properties.</p>""" + ) + ) + self.userPropsAct.triggered.connect(self.__showUserProperties) + self.actions.append(self.userPropsAct) + + self.filetypesAct = EricAction( + self.tr("Filetype Associations"), + self.tr("Filetype Associations..."), + 0, + 0, + self, + "project_filetype_associations", + ) + self.filetypesAct.setStatusTip( + self.tr("Show the project file type associations") + ) + self.filetypesAct.setWhatsThis( + self.tr( + """<b>Filetype Associations...</b>""" + """<p>This shows a dialog to edit the file type associations of""" + """ the project. These associations determine the type""" + """ (source, form, interface, protocol or others) with a""" + """ filename pattern. They are used when adding a file to the""" + """ project and when performing a search for new files.</p>""" + ) + ) + self.filetypesAct.triggered.connect(self.__showFiletypeAssociations) + self.actions.append(self.filetypesAct) + + self.lexersAct = EricAction( + self.tr("Lexer Associations"), + self.tr("Lexer Associations..."), + 0, + 0, + self, + "project_lexer_associations", + ) + self.lexersAct.setStatusTip( + self.tr("Show the project lexer associations (overriding defaults)") + ) + self.lexersAct.setWhatsThis( + self.tr( + """<b>Lexer Associations...</b>""" + """<p>This shows a dialog to edit the lexer associations of""" + """ the project. These associations override the global lexer""" + """ associations. Lexers are used to highlight the editor""" + """ text.</p>""" + ) + ) + self.lexersAct.triggered.connect(self.__showLexerAssociations) + self.actions.append(self.lexersAct) + + ################################################################### + ## Project debug actions + ################################################################### + + self.dbgActGrp = createActionGroup(self) + + act = EricAction( + self.tr("Debugger Properties"), + self.tr("Debugger &Properties..."), + 0, + 0, + self.dbgActGrp, + "project_debugger_properties", + ) + act.setStatusTip(self.tr("Show the debugger properties")) + act.setWhatsThis( + self.tr( + """<b>Debugger Properties...</b>""" + """<p>This shows a dialog to edit project specific debugger""" + """ settings.</p>""" + ) + ) + act.triggered.connect(self.__showDebugProperties) + self.actions.append(act) + + act = EricAction( + self.tr("Load"), + self.tr("&Load"), + 0, + 0, + self.dbgActGrp, + "project_debugger_properties_load", + ) + act.setStatusTip(self.tr("Load the debugger properties")) + act.setWhatsThis( + self.tr( + """<b>Load Debugger Properties</b>""" + """<p>This loads the project specific debugger settings.</p>""" + ) + ) + act.triggered.connect(self.__readDebugProperties) + self.actions.append(act) + + act = EricAction( + self.tr("Save"), + self.tr("&Save"), + 0, + 0, + self.dbgActGrp, + "project_debugger_properties_save", + ) + act.setStatusTip(self.tr("Save the debugger properties")) + act.setWhatsThis( + self.tr( + """<b>Save Debugger Properties</b>""" + """<p>This saves the project specific debugger settings.</p>""" + ) + ) + act.triggered.connect(self.__writeDebugProperties) + self.actions.append(act) + + act = EricAction( + self.tr("Delete"), + self.tr("&Delete"), + 0, + 0, + self.dbgActGrp, + "project_debugger_properties_delete", + ) + act.setStatusTip(self.tr("Delete the debugger properties")) + act.setWhatsThis( + self.tr( + """<b>Delete Debugger Properties</b>""" + """<p>This deletes the file containing the project specific""" + """ debugger settings.</p>""" + ) + ) + act.triggered.connect(self.__deleteDebugProperties) + self.actions.append(act) + + act = EricAction( + self.tr("Reset"), + self.tr("&Reset"), + 0, + 0, + self.dbgActGrp, + "project_debugger_properties_resets", + ) + act.setStatusTip(self.tr("Reset the debugger properties")) + act.setWhatsThis( + self.tr( + """<b>Reset Debugger Properties</b>""" + """<p>This resets the project specific debugger settings.</p>""" + ) + ) + act.triggered.connect(self.__initDebugProperties) + self.actions.append(act) + + ################################################################### + ## Project session actions + ################################################################### + + self.sessActGrp = createActionGroup(self) + + act = EricAction( + self.tr("Load session"), + self.tr("Load session"), + 0, + 0, + self.sessActGrp, + "project_load_session", + ) + act.setStatusTip(self.tr("Load the projects session file.")) + act.setWhatsThis( + self.tr( + """<b>Load session</b>""" + """<p>This loads the projects session file. The session consists""" + """ of the following data.<br>""" + """- all open source files<br>""" + """- all breakpoint<br>""" + """- the commandline arguments<br>""" + """- the working directory<br>""" + """- the exception reporting flag</p>""" + ) + ) + act.triggered.connect(self.__readSession) + self.actions.append(act) + + act = EricAction( + self.tr("Save session"), + self.tr("Save session"), + 0, + 0, + self.sessActGrp, + "project_save_session", + ) + act.setStatusTip(self.tr("Save the projects session file.")) + act.setWhatsThis( + self.tr( + """<b>Save session</b>""" + """<p>This saves the projects session file. The session consists""" + """ of the following data.<br>""" + """- all open source files<br>""" + """- all breakpoint<br>""" + """- the commandline arguments<br>""" + """- the working directory<br>""" + """- the exception reporting flag</p>""" + ) + ) + act.triggered.connect(self.__writeSession) + self.actions.append(act) + + act = EricAction( + self.tr("Delete session"), + self.tr("Delete session"), + 0, + 0, + self.sessActGrp, + "project_delete_session", + ) + act.setStatusTip(self.tr("Delete the projects session file.")) + act.setWhatsThis( + self.tr( + """<b>Delete session</b>""" + """<p>This deletes the projects session file</p>""" + ) + ) + act.triggered.connect(self.__deleteSession) + self.actions.append(act) + + ################################################################### + ## Project Tools - check actions + ################################################################### + + self.chkGrp = createActionGroup(self) + + self.codeMetricsAct = EricAction( + self.tr("Code Metrics"), + self.tr("&Code Metrics..."), + 0, + 0, + self.chkGrp, + "project_code_metrics", + ) + self.codeMetricsAct.setStatusTip( + self.tr("Show some code metrics for the project.") + ) + self.codeMetricsAct.setWhatsThis( + self.tr( + """<b>Code Metrics...</b>""" + """<p>This shows some code metrics for all Python files in""" + """ the project.</p>""" + ) + ) + self.codeMetricsAct.triggered.connect(self.__showCodeMetrics) + self.actions.append(self.codeMetricsAct) + + self.codeCoverageAct = EricAction( + self.tr("Python Code Coverage"), + self.tr("Code Co&verage..."), + 0, + 0, + self.chkGrp, + "project_code_coverage", + ) + self.codeCoverageAct.setStatusTip( + self.tr("Show code coverage information for the project.") + ) + self.codeCoverageAct.setWhatsThis( + self.tr( + """<b>Code Coverage...</b>""" + """<p>This shows the code coverage information for all Python""" + """ files in the project.</p>""" + ) + ) + self.codeCoverageAct.triggered.connect(self.__showCodeCoverage) + self.actions.append(self.codeCoverageAct) + + self.codeProfileAct = EricAction( + self.tr("Profile Data"), + self.tr("&Profile Data..."), + 0, + 0, + self.chkGrp, + "project_profile_data", + ) + self.codeProfileAct.setStatusTip( + self.tr("Show profiling data for the project.") + ) + self.codeProfileAct.setWhatsThis( + self.tr( + """<b>Profile Data...</b>""" + """<p>This shows the profiling data for the project.</p>""" + ) + ) + self.codeProfileAct.triggered.connect(self.__showProfileData) + self.actions.append(self.codeProfileAct) + + ################################################################### + ## Project Tools - graphics actions + ################################################################### + + self.graphicsGrp = createActionGroup(self) + + self.applicationDiagramAct = EricAction( + self.tr("Application Diagram"), + self.tr("&Application Diagram..."), + 0, + 0, + self.graphicsGrp, + "project_application_diagram", + ) + self.applicationDiagramAct.setStatusTip( + self.tr("Show a diagram of the project.") + ) + self.applicationDiagramAct.setWhatsThis( + self.tr( + """<b>Application Diagram...</b>""" + """<p>This shows a diagram of the project.</p>""" + ) + ) + self.applicationDiagramAct.triggered.connect(self.handleApplicationDiagram) + self.actions.append(self.applicationDiagramAct) + + self.loadDiagramAct = EricAction( + self.tr("Load Diagram"), + self.tr("&Load Diagram..."), + 0, + 0, + self.graphicsGrp, + "project_load_diagram", + ) + self.loadDiagramAct.setStatusTip(self.tr("Load a diagram from file.")) + self.loadDiagramAct.setWhatsThis( + self.tr( + """<b>Load Diagram...</b>""" + """<p>This loads a diagram from file.</p>""" + ) + ) + self.loadDiagramAct.triggered.connect(self.__loadDiagram) + self.actions.append(self.loadDiagramAct) + + ################################################################### + ## Project Tools - plugin packaging actions + ################################################################### + + self.pluginGrp = createActionGroup(self) + + self.pluginPkgListAct = EricAction( + self.tr("Create Package List"), + UI.PixmapCache.getIcon("pluginArchiveList"), + self.tr("Create &Package List"), + 0, + 0, + self.pluginGrp, + "project_plugin_pkglist", + ) + self.pluginPkgListAct.setStatusTip( + self.tr("Create an initial PKGLIST file for an eric plugin.") + ) + self.pluginPkgListAct.setWhatsThis( + self.tr( + """<b>Create Package List</b>""" + """<p>This creates an initial list of files to include in an""" + """ eric plugin archive. The list is created from the project""" + """ file.</p>""" + ) + ) + self.pluginPkgListAct.triggered.connect(self.__pluginCreatePkgList) + self.actions.append(self.pluginPkgListAct) + + self.pluginArchiveAct = EricAction( + self.tr("Create Plugin Archives"), + UI.PixmapCache.getIcon("pluginArchive"), + self.tr("Create Plugin &Archives"), + 0, + 0, + self.pluginGrp, + "project_plugin_archive", + ) + self.pluginArchiveAct.setStatusTip(self.tr("Create eric plugin archive files.")) + self.pluginArchiveAct.setWhatsThis( + self.tr( + """<b>Create Plugin Archives</b>""" + """<p>This creates eric plugin archive files using the list""" + """ of files given in a PKGLIST* file. The archive name is""" + """ built from the main script name if not designated in""" + """ the package list file.</p>""" + ) + ) + self.pluginArchiveAct.triggered.connect(self.__pluginCreateArchives) + self.actions.append(self.pluginArchiveAct) + + self.pluginSArchiveAct = EricAction( + self.tr("Create Plugin Archives (Snapshot)"), + UI.PixmapCache.getIcon("pluginArchiveSnapshot"), + self.tr("Create Plugin Archives (&Snapshot)"), + 0, + 0, + self.pluginGrp, + "project_plugin_sarchive", + ) + self.pluginSArchiveAct.setStatusTip( + self.tr("Create eric plugin archive files (snapshot releases).") + ) + self.pluginSArchiveAct.setWhatsThis( + self.tr( + """<b>Create Plugin Archives (Snapshot)</b>""" + """<p>This creates eric plugin archive files using the list""" + """ of files given in the PKGLIST* file. The archive name is""" + """ built from the main script name if not designated in""" + """ the package list file. The version entry of the main script""" + """ is modified to reflect a snapshot release.</p>""" + ) + ) + self.pluginSArchiveAct.triggered.connect(self.__pluginCreateSnapshotArchives) + self.actions.append(self.pluginSArchiveAct) + + ################################################################### + ## Project Tools - make actions + ################################################################### + + self.makeGrp = createActionGroup(self) + + self.makeExecuteAct = EricAction( + self.tr("Execute Make"), + self.tr("&Execute Make"), + 0, + 0, + self.makeGrp, + "project_make_execute", + ) + self.makeExecuteAct.setStatusTip(self.tr("Perform a 'make' run.")) + self.makeExecuteAct.setWhatsThis( + self.tr( + """<b>Execute Make</b>""" + """<p>This performs a 'make' run to rebuild the configured""" + """ target.</p>""" + ) + ) + self.makeExecuteAct.triggered.connect(self.__executeMake) + self.actions.append(self.makeExecuteAct) + + self.makeTestAct = EricAction( + self.tr("Test for Changes"), + self.tr("&Test for Changes"), + 0, + 0, + self.makeGrp, + "project_make_test", + ) + self.makeTestAct.setStatusTip( + self.tr("Question 'make', if a rebuild is needed.") + ) + self.makeTestAct.setWhatsThis( + self.tr( + """<b>Test for Changes</b>""" + """<p>This questions 'make', if a rebuild of the configured""" + """ target is necessary.</p>""" + ) + ) + self.makeTestAct.triggered.connect( + lambda: self.__executeMake(questionOnly=True) + ) + self.actions.append(self.makeTestAct) + + ################################################################### + ## Project Tools - other tools actions + ################################################################### + + self.othersGrp = createActionGroup(self) + + self.createSBOMAct = EricAction( + self.tr("Create SBOM File"), + self.tr("Create &SBOM File"), + 0, + 0, + self.othersGrp, + "project_create_sbom", + ) + self.createSBOMAct.setStatusTip( + self.tr("Create a SBOM file of the project dependencies.") + ) + self.createSBOMAct.setWhatsThis( + self.tr( + """<b>Create SBOM File</b>""" + """<p>This allows the creation of a SBOM file of the project""" + """ dependencies. This may be based on various input sources""" + """ and will be saved as a CycloneDX SBOM file.</p>""" + ) + ) + self.createSBOMAct.triggered.connect(self.__createSBOMFile) + self.actions.append(self.createSBOMAct) + + ################################################################### + ## Project Tools - code formatting actions + ################################################################### + + self.blackFormattingGrp = createActionGroup(self) + + self.blackAboutAct = EricAction( + self.tr("About Black"), + self.tr("&Black"), + 0, + 0, + self.blackFormattingGrp, + "project_black_about", + ) + self.blackAboutAct.setStatusTip(self.tr("Show some information about 'Black'.")) + self.blackAboutAct.setWhatsThis( + self.tr( + "<b>Black</b>" + "<p>This shows some information about the installed 'Black' tool.</p>" + ) + ) + self.blackAboutAct.triggered.connect(self.__aboutBlack) + self.actions.append(self.blackAboutAct) + font = self.blackAboutAct.font() + font.setBold(True) + self.blackAboutAct.setFont(font) + + self.blackFormatAct = EricAction( + self.tr("Format Code"), + self.tr("&Format Code"), + 0, + 0, + self.blackFormattingGrp, + "project_black_format_code", + ) + self.blackFormatAct.setStatusTip( + self.tr("Format the project sources with 'Black'.") + ) + self.blackFormatAct.setWhatsThis( + self.tr( + "<b>Format Code</b>" + "<p>This shows a dialog to enter parameters for the formatting run and" + " reformats the project sources using 'Black'.</p>" + ) + ) + self.blackFormatAct.triggered.connect( + lambda: self.__performFormatWithBlack(BlackFormattingAction.Format) + ) + self.actions.append(self.blackFormatAct) + + self.blackCheckFormattingAct = EricAction( + self.tr("Check Code Formatting"), + self.tr("&Check Code Formatting"), + 0, + 0, + self.blackFormattingGrp, + "project_black_check_code", + ) + self.blackCheckFormattingAct.setStatusTip( + self.tr( + "Check, if the project sources need to be reformatted with 'Black'." + ) + ) + self.blackCheckFormattingAct.setWhatsThis( + self.tr( + "<b>Check Code Formatting</b>" + "<p>This shows a dialog to enter parameters for the format check run" + " and performs a check, if the project sources need to be reformatted" + " using 'Black'.</p>" + ) + ) + self.blackCheckFormattingAct.triggered.connect( + lambda: self.__performFormatWithBlack(BlackFormattingAction.Check) + ) + self.actions.append(self.blackCheckFormattingAct) + + self.blackDiffFormattingAct = EricAction( + self.tr("Code Formatting Diff"), + self.tr("Code Formatting &Diff"), + 0, + 0, + self.blackFormattingGrp, + "project_black_diff_code", + ) + self.blackDiffFormattingAct.setStatusTip( + self.tr( + "Generate a unified diff of potential project source reformatting" + " with 'Black'." + ) + ) + self.blackDiffFormattingAct.setWhatsThis( + self.tr( + "<b>Diff Code Formatting</b>" + "<p>This shows a dialog to enter parameters for the format diff run and" + " generates a unified diff of potential project source reformatting" + " using 'Black'.</p>" + ) + ) + self.blackDiffFormattingAct.triggered.connect( + lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff) + ) + self.actions.append(self.blackDiffFormattingAct) + + self.closeAct.setEnabled(False) + self.saveAct.setEnabled(False) + self.saveasAct.setEnabled(False) + self.actGrp2.setEnabled(False) + self.propsAct.setEnabled(False) + self.userPropsAct.setEnabled(False) + self.filetypesAct.setEnabled(False) + self.lexersAct.setEnabled(False) + self.sessActGrp.setEnabled(False) + self.dbgActGrp.setEnabled(False) + self.pluginGrp.setEnabled(False) + + def initMenus(self): + """ + Public slot to initialize the project menus. + + @return tuple of generated menus + @rtype tuple of (QMenu, QMenu) + """ + menu = QMenu(self.tr("&Project"), self.parent()) + self.recentMenu = QMenu(self.tr("Open &Recent Projects"), menu) + self.sessionMenu = QMenu(self.tr("Session"), menu) + self.debuggerMenu = QMenu(self.tr("Debugger"), menu) + + toolsMenu = QMenu(self.tr("Project-T&ools"), self.parent()) + self.vcsMenu = QMenu(self.tr("&Version Control"), toolsMenu) + self.vcsMenu.setTearOffEnabled(True) + self.vcsProjectHelper.initMenu(self.vcsMenu) + self.vcsMenu.setEnabled(self.vcsSoftwareAvailable()) + self.checksMenu = QMenu(self.tr("Chec&k"), toolsMenu) + self.checksMenu.setTearOffEnabled(True) + self.formattingMenu = QMenu(self.tr("Code &Formatting"), toolsMenu) + self.formattingMenu.setTearOffEnabled(True) + self.menuShow = QMenu(self.tr("Sho&w"), toolsMenu) + self.graphicsMenu = QMenu(self.tr("&Diagrams"), toolsMenu) + self.packagersMenu = QMenu(self.tr("Pac&kagers"), toolsMenu) + self.apidocMenu = QMenu(self.tr("Source &Documentation"), toolsMenu) + self.apidocMenu.setTearOffEnabled(True) + self.makeMenu = QMenu(self.tr("Make"), toolsMenu) + self.othersMenu = QMenu(self.tr("Other Tools"), toolsMenu) + + self.__menus = { + "Main": menu, + "Recent": self.recentMenu, + "VCS": self.vcsMenu, + "Checks": self.checksMenu, + "Show": self.menuShow, + "Graphics": self.graphicsMenu, + "Session": self.sessionMenu, + "Apidoc": self.apidocMenu, + "Debugger": self.debuggerMenu, + "Packagers": self.packagersMenu, + "Make": self.makeMenu, + "OtherTools": self.othersMenu, + "Formatting": self.formattingMenu, + } + + # connect the aboutToShow signals + self.recentMenu.aboutToShow.connect(self.__showContextMenuRecent) + self.recentMenu.triggered.connect(self.__openRecent) + self.vcsMenu.aboutToShow.connect(self.__showContextMenuVCS) + self.checksMenu.aboutToShow.connect(self.__showContextMenuChecks) + self.menuShow.aboutToShow.connect(self.__showContextMenuShow) + self.graphicsMenu.aboutToShow.connect(self.__showContextMenuGraphics) + self.apidocMenu.aboutToShow.connect(self.__showContextMenuApiDoc) + self.packagersMenu.aboutToShow.connect(self.__showContextMenuPackagers) + self.sessionMenu.aboutToShow.connect(self.__showContextMenuSession) + self.debuggerMenu.aboutToShow.connect(self.__showContextMenuDebugger) + self.makeMenu.aboutToShow.connect(self.__showContextMenuMake) + self.othersMenu.aboutToShow.connect(self.__showContextMenuOthers) + self.formattingMenu.aboutToShow.connect(self.__showContextMenuFormat) + menu.aboutToShow.connect(self.__showMenu) + + # build the show menu + self.menuShow.setTearOffEnabled(True) + self.menuShow.addAction(self.codeMetricsAct) + self.menuShow.addAction(self.codeCoverageAct) + self.menuShow.addAction(self.codeProfileAct) + + # build the diagrams menu + self.graphicsMenu.setTearOffEnabled(True) + self.graphicsMenu.addAction(self.applicationDiagramAct) + self.graphicsMenu.addSeparator() + self.graphicsMenu.addAction(self.loadDiagramAct) + + # build the session menu + self.sessionMenu.setTearOffEnabled(True) + self.sessionMenu.addActions(self.sessActGrp.actions()) + + # build the debugger menu + self.debuggerMenu.setTearOffEnabled(True) + self.debuggerMenu.addActions(self.dbgActGrp.actions()) + + # build the packagers menu + self.packagersMenu.setTearOffEnabled(True) + self.packagersMenu.addActions(self.pluginGrp.actions()) + self.packagersMenu.addSeparator() + + # build the make menu + self.makeMenu.setTearOffEnabled(True) + self.makeMenu.addActions(self.makeGrp.actions()) + self.makeMenu.addSeparator() + + # build the 'Other Tools' menu + self.othersMenu.setTearOffEnabled(True) + self.othersMenu.addActions(self.othersGrp.actions()) + self.othersMenu.addSeparator() + + # build the 'Code Formatting' menu + self.formattingMenu.setTearOffEnabled(True) + self.formattingMenu.addActions(self.blackFormattingGrp.actions()) + self.formattingMenu.addSeparator() + + # build the project 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.addActions(self.actGrp2.actions()) + menu.addSeparator() + menu.addAction(self.propsAct) + menu.addAction(self.userPropsAct) + menu.addAction(self.filetypesAct) + menu.addAction(self.lexersAct) + menu.addSeparator() + self.menuDebuggerAct = menu.addMenu(self.debuggerMenu) + self.menuSessionAct = menu.addMenu(self.sessionMenu) + + # build the project tools menu + toolsMenu.setTearOffEnabled(True) + toolsMenu.addSeparator() + toolsMenu.addMenu(self.vcsMenu) + toolsMenu.addSeparator() + self.menuCheckAct = toolsMenu.addMenu(self.checksMenu) + toolsMenu.addSeparator() + self.menuFormattingAct = toolsMenu.addMenu(self.formattingMenu) + toolsMenu.addSeparator() + self.menuMakeAct = toolsMenu.addMenu(self.makeMenu) + toolsMenu.addSeparator() + self.menuDiagramAct = toolsMenu.addMenu(self.graphicsMenu) + toolsMenu.addSeparator() + self.menuShowAct = toolsMenu.addMenu(self.menuShow) + toolsMenu.addSeparator() + self.menuApidocAct = toolsMenu.addMenu(self.apidocMenu) + toolsMenu.addSeparator() + self.menuPackagersAct = toolsMenu.addMenu(self.packagersMenu) + toolsMenu.addSeparator() + self.menuOtherToolsAct = toolsMenu.addMenu(self.othersMenu) + + self.menuCheckAct.setEnabled(False) + self.menuShowAct.setEnabled(False) + self.menuDiagramAct.setEnabled(False) + self.menuSessionAct.setEnabled(False) + self.menuDebuggerAct.setEnabled(False) + self.menuApidocAct.setEnabled(False) + self.menuPackagersAct.setEnabled(False) + self.menuMakeAct.setEnabled(False) + self.menuOtherToolsAct.setEnabled(False) + self.menuFormattingAct.setEnabled(False) + + self.__menu = menu + self.__toolsMenu = toolsMenu + + return menu, toolsMenu + + def initToolbars(self, toolbarManager): + """ + Public slot to initialize the project toolbar and the basic VCS + toolbar. + + @param toolbarManager reference to a toolbar manager object + (EricToolBarManager) + @return tuple of the generated toolbars (tuple of two QToolBar) + """ + tb = QToolBar(self.tr("Project"), self.ui) + tb.setIconSize(UI.Config.ToolBarIconSize) + tb.setObjectName("ProjectToolbar") + tb.setToolTip(self.tr("Project")) + + 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.addFilesAct, tb.windowTitle()) + toolbarManager.addAction(self.addDirectoryAct, tb.windowTitle()) + toolbarManager.addAction(self.addLanguageAct, tb.windowTitle()) + toolbarManager.addAction(self.propsAct, tb.windowTitle()) + toolbarManager.addAction(self.userPropsAct, tb.windowTitle()) + + import VCS + + vcstb = VCS.getBasicHelper(self).initBasicToolbar(self.ui, toolbarManager) + + return tb, vcstb + + def __showMenu(self): + """ + Private method to set up the 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 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 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(pathlib.Path(rp).exists()) + + self.recentMenu.addSeparator() + self.recentMenu.addAction(self.tr("&Clear"), self.clearRecent) + + def __openRecent(self, act): + """ + Private method to open a project from the list of rencently opened + projects. + + @param act reference to the action that triggered (QAction) + """ + file = act.data() + if file: + self.openProject(file) + + def clearRecent(self): + """ + Public method to clear the recent projects menu. + """ + self.recent = [] + self.__saveRecent() + + def clearHistories(self): + """ + Public method to clear the project related histories. + """ + self.clearRecent() + + for key in ["DebugClientsHistory", "DebuggerInterpreterHistory"]: + Preferences.setProject(key, []) + Preferences.syncPreferences() + + def __searchNewFiles(self): + """ + Private slot used to handle the search new files action. + """ + self.__doSearchNewFiles(False, True) + + def __searchProjectFile(self): + """ + Private slot to show the Find Project File dialog. + """ + if self.__findProjectFileDialog is None: + from .QuickFindFileDialog import QuickFindFileDialog + + self.__findProjectFileDialog = QuickFindFileDialog(self) + self.__findProjectFileDialog.sourceFile.connect(self.sourceFile) + self.__findProjectFileDialog.designerFile.connect(self.designerFile) + self.__findProjectFileDialog.linguistFile.connect(self.linguistFile) + self.__findProjectFileDialog.show() + self.__findProjectFileDialog.raise_() + self.__findProjectFileDialog.activateWindow() + + def __doSearchNewFiles(self, AI=True, onUserDemand=False): + """ + Private method to search for new files in the project directory. + + If new files were found, it shows a dialog listing these files and + gives the user the opportunity to select the ones he wants to + include. If 'Automatic Inclusion' is enabled, the new files are + automatically added to the project. + + @param AI flag indicating whether the automatic inclusion should + be honoured (boolean) + @param onUserDemand flag indicating whether this method was + requested by the user via a menu action (boolean) + """ + autoInclude = Preferences.getProject("AutoIncludeNewFiles") + recursiveSearch = Preferences.getProject("SearchNewFilesRecursively") + newFiles = [] + + ignore_patterns = [ + pattern + for pattern, filetype in self.pdata["FILETYPES"].items() + if filetype == "__IGNORE__" + ] + + dirs = self.subdirs[:] + for directory in dirs: + skip = False + for ignore_pattern in ignore_patterns: + if fnmatch.fnmatch(directory, ignore_pattern): + skip = True + break + if skip: + continue + + curpath = os.path.join(self.ppath, directory) + try: + newSources = os.listdir(curpath) + except OSError: + newSources = [] + pattern = ( + self.pdata["TRANSLATIONPATTERN"].replace("%language%", "*") + if self.pdata["TRANSLATIONPATTERN"] + else "*.ts" + ) + binpattern = self.__binaryTranslationFile(pattern) + for ns in newSources: + # ignore hidden files and directories + if ns.startswith("."): + continue + if ( + Utilities.isWindowsPlatform() + and os.path.isdir(os.path.join(curpath, ns)) + and ns.startswith("_") + ): + # dot net hack + continue + + # set fn to project relative name + # then reset ns to fully qualified name for insertion, + # possibly. + fn = os.path.join(directory, ns) if directory else ns + ns = os.path.abspath(os.path.join(curpath, ns)) + + # do not bother with dirs here... + if os.path.isdir(ns): + if recursiveSearch: + d = self.getRelativePath(ns) + if d not in dirs: + dirs.append(d) + continue + + filetype = "" + bfn = os.path.basename(fn) + for pattern in sorted(self.pdata["FILETYPES"].keys(), reverse=True): + if fnmatch.fnmatch(bfn, pattern): + filetype = self.pdata["FILETYPES"][pattern] + break + + if ( + (filetype == "SOURCES" and fn not in self.pdata["SOURCES"]) + or (filetype == "FORMS" and fn not in self.pdata["FORMS"]) + or (filetype == "INTERFACES" and fn not in self.pdata["INTERFACES"]) + or (filetype == "PROTOCOLS" and fn not in self.pdata["PROTOCOLS"]) + or (filetype == "RESOURCES" and fn not in self.pdata["RESOURCES"]) + or (filetype == "OTHERS" and fn not in self.pdata["OTHERS"]) + or ( + filetype == "TRANSLATIONS" + and fn not in self.pdata["TRANSLATIONS"] + and ( + fnmatch.fnmatch(ns, pattern) + or fnmatch.fnmatch(ns, binpattern) + ) + ) + ): + if autoInclude and AI: + self.appendFile(ns) + else: + newFiles.append(ns) + + # if autoInclude is set there is no more work left + if autoInclude and AI: + return + + # if newfiles is empty, put up message box informing user nothing found + if not newFiles: + if onUserDemand: + EricMessageBox.information( + self.ui, + self.tr("Search New Files"), + self.tr("There were no new files found to be added."), + ) + return + + # autoInclude is not set, show a dialog + from .AddFoundFilesDialog import AddFoundFilesDialog + + dlg = AddFoundFilesDialog(newFiles, self.parent(), None) + res = dlg.exec() + + # the 'Add All' button was pressed + if res == 1: + for file in newFiles: + self.appendFile(file) + + # the 'Add Selected' button was pressed + elif res == 2: + files = dlg.getSelection() + for file in files: + self.appendFile(file) + + def othersAdded(self, fn, updateModel=True): + """ + Public slot to be called, if something was added to the OTHERS project + data area. + + @param fn filename or directory name added (string) + @param updateModel flag indicating an update of the model is requested + (boolean) + """ + self.projectOthersAdded.emit(fn) + updateModel and self.__model.addNewItem("OTHERS", fn) + + def getActions(self): + """ + Public method to get a list of all actions. + + @return list of all actions (list of EricAction) + """ + return self.actions[:] + + def addEricActions(self, actions): + """ + Public method to add actions to the list of actions. + + @param actions list of actions (list of EricAction) + """ + self.actions.extend(actions) + + def removeEricActions(self, actions): + """ + Public method to remove actions from the list of actions. + + @param actions list of actions (list of EricAction) + """ + 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 repopulateItem(self, fullname): + """ + Public slot to repopulate a named item. + + @param fullname full name of the item to repopulate (string) + """ + if not self.isOpen(): + return + + with EricOverrideCursor(): + name = self.getRelativePath(fullname) + self.prepareRepopulateItem.emit(name) + self.__model.repopulateItem(name) + self.completeRepopulateItem.emit(name) + + ############################################################## + ## Below is the VCS interface + ############################################################## + + def initVCS(self, vcsSystem=None, nooverride=False): + """ + Public method used to instantiate a vcs system. + + @param vcsSystem type of VCS to be used (string) + @param nooverride flag indicating to ignore an override request + (boolean) + @return a reference to the vcs object + """ + vcs = None + forProject = True + override = False + + if vcsSystem is None: + if self.pdata["VCS"] and self.pdata["VCS"] != "None": + vcsSystem = self.pdata["VCS"] + else: + forProject = False + + if ( + forProject + and self.pdata["VCS"] + and self.pdata["VCS"] != "None" + and self.pudata["VCSOVERRIDE"] + and not nooverride + ): + vcsSystem = self.pudata["VCSOVERRIDE"] + override = True + + if vcsSystem is not None: + import VCS + + try: + vcs = VCS.factory(vcsSystem) + except ImportError: + if override: + # override failed, revert to original + self.pudata["VCSOVERRIDE"] = "" + return self.initVCS(nooverride=True) + + if vcs: + vcsExists, msg = vcs.vcsExists() + if not vcsExists: + if override: + # override failed, revert to original + with EricOverridenCursor(): + EricMessageBox.critical( + self.ui, + self.tr("Version Control System"), + self.tr( + "<p>The selected VCS <b>{0}</b> could not be" + " found. <br/>Reverting override.</p><p>{1}" + "</p>" + ).format(vcsSystem, msg), + ) + self.pudata["VCSOVERRIDE"] = "" + return self.initVCS(nooverride=True) + + with EricOverridenCursor(): + EricMessageBox.critical( + self.ui, + self.tr("Version Control System"), + self.tr( + "<p>The selected VCS <b>{0}</b> could not be" + " found.<br/>Disabling version control.</p>" + "<p>{1}</p>" + ).format(vcsSystem, msg), + ) + vcs = None + if forProject: + self.pdata["VCS"] = "None" + self.setDirty(True) + else: + vcs.vcsInitConfig(self) + + if vcs and forProject: + # set the vcs options + if vcs.vcsSupportCommandOptions(): + with contextlib.suppress(LookupError): + vcsopt = copy.deepcopy(self.pdata["VCSOPTIONS"]) + vcs.vcsSetOptions(vcsopt) + # set vcs specific data + with contextlib.suppress(LookupError): + vcsother = copy.deepcopy(self.pdata["VCSOTHERDATA"]) + vcs.vcsSetOtherData(vcsother) + + if forProject: + if vcs is None: + import VCS + + self.vcsProjectHelper = VCS.getBasicHelper(self) + self.vcsBasicHelper = True + else: + self.vcsProjectHelper = vcs.vcsGetProjectHelper(self) + self.vcsBasicHelper = False + if self.vcsMenu is not None: + self.vcsProjectHelper.initMenu(self.vcsMenu) + self.vcsMenu.setEnabled(self.vcsSoftwareAvailable()) + + return vcs + + def resetVCS(self): + """ + Public method to reset the VCS. + """ + self.pdata["VCS"] = "None" + self.vcs = self.initVCS() + ericApp().getObject("PluginManager").deactivateVcsPlugins() + + def __showContextMenuVCS(self): + """ + Private slot called before the vcs menu is shown. + """ + self.vcsProjectHelper.showMenu() + if self.vcsBasicHelper: + self.showMenu.emit("VCS", self.vcsMenu) + + def vcsSoftwareAvailable(self): + """ + Public method to check, if some supported VCS software is available + to the IDE. + + @return flag indicating availability of VCS software (boolean) + """ + vcsSystemsDict = ( + ericApp() + .getObject("PluginManager") + .getPluginDisplayStrings("version_control") + ) + return len(vcsSystemsDict) != 0 + + def __vcsStatusChanged(self): + """ + Private slot to handle a change of the overall VCS status. + """ + self.projectChanged.emit() + + def __vcsConnectStatusMonitor(self): + """ + Private method to start the VCS monitor and connect its signals. + """ + if self.vcs is not None: + self.vcs.committed.connect(self.vcsCommitted) + + self.vcs.startStatusMonitor(self) + self.vcs.vcsStatusMonitorData.connect(self.__model.changeVCSStates) + self.vcs.vcsStatusMonitorData.connect(self.vcsStatusMonitorData) + self.vcs.vcsStatusMonitorAllData.connect(self.vcsStatusMonitorAllData) + self.vcs.vcsStatusMonitorStatus.connect(self.vcsStatusMonitorStatus) + self.vcs.vcsStatusMonitorInfo.connect(self.vcsStatusMonitorInfo) + self.vcs.vcsStatusChanged.connect(self.__vcsStatusChanged) + + ######################################################################### + ## Below is the interface to the checker tools + ######################################################################### + + def __showContextMenuChecks(self): + """ + Private slot called before the checks menu is shown. + """ + self.showMenu.emit("Checks", self.checksMenu) + + ######################################################################### + ## Below is the interface to the packagers tools + ######################################################################### + + def __showContextMenuPackagers(self): + """ + Private slot called before the packagers menu is shown. + """ + self.showMenu.emit("Packagers", self.packagersMenu) + + ######################################################################### + ## Below is the interface to the apidoc tools + ######################################################################### + + def __showContextMenuApiDoc(self): + """ + Private slot called before the apidoc menu is shown. + """ + self.showMenu.emit("Apidoc", self.apidocMenu) + + ######################################################################### + ## Below is the interface to the show tools + ######################################################################### + + def __showCodeMetrics(self): + """ + Private slot used to calculate some code metrics for the project files. + """ + files = [ + os.path.join(self.ppath, file) + for file in self.pdata["SOURCES"] + if file.endswith(".py") + ] + from DataViews.CodeMetricsDialog import CodeMetricsDialog + + self.codemetrics = CodeMetricsDialog() + self.codemetrics.show() + self.codemetrics.prepare(files) + + def __showCodeCoverage(self): + """ + Private slot used to show the code coverage information for the + project files. + """ + fn = self.getMainScript(True) + if fn is None: + EricMessageBox.critical( + self.ui, + self.tr("Coverage Data"), + self.tr( + "There is no main script defined for the" + " current project. Aborting" + ), + ) + return + + files = Utilities.getCoverageFileNames(fn) + if files: + if len(files) > 1: + fn, ok = QInputDialog.getItem( + None, + self.tr("Code Coverage"), + self.tr("Please select a coverage file"), + files, + 0, + False, + ) + if not ok: + return + else: + fn = files[0] + else: + return + + files = [ + os.path.join(self.ppath, file) + for file in self.pdata["SOURCES"] + if os.path.splitext(file)[1].startswith(".py") + ] + from DataViews.PyCoverageDialog import PyCoverageDialog + + self.codecoverage = PyCoverageDialog() + self.codecoverage.show() + self.codecoverage.start(fn, files) + + def __showProfileData(self): + """ + Private slot used to show the profiling information for the project. + """ + fn = self.getMainScript(True) + if fn is None: + EricMessageBox.critical( + self.ui, + self.tr("Profile Data"), + self.tr( + "There is no main script defined for the" + " current project. Aborting" + ), + ) + return + + files = Utilities.getProfileFileNames(fn) + if files: + if len(files) > 1: + fn, ok = QInputDialog.getItem( + None, + self.tr("Profile Data"), + self.tr("Please select a profile file"), + files, + 0, + False, + ) + if not ok: + return + else: + fn = files[0] + else: + return + + from DataViews.PyProfileDialog import PyProfileDialog + + self.profiledata = PyProfileDialog() + self.profiledata.show() + self.profiledata.start(fn) + + def __showContextMenuShow(self): + """ + Private slot called before the show menu is shown. + """ + fn = self.getMainScript(True) + if not fn: + fn = self.getProjectPath() + + self.codeProfileAct.setEnabled( + self.isPy3Project() and bool(Utilities.getProfileFileName(fn)) + ) + self.codeCoverageAct.setEnabled( + self.isPy3Project() and bool(Utilities.getCoverageFileNames(fn)) + ) + + self.showMenu.emit("Show", self.menuShow) + + ######################################################################### + ## Below is the interface to the diagrams + ######################################################################### + + def __showContextMenuGraphics(self): + """ + Private slot called before the graphics menu is shown. + """ + self.showMenu.emit("Graphics", self.graphicsMenu) + + def handleApplicationDiagram(self): + """ + Public method to handle the application diagram context menu action. + """ + res = EricMessageBox.yesNo( + self.ui, + self.tr("Application Diagram"), + self.tr("""Include module names?"""), + yesDefault=True, + ) + + from Graphics.UMLDialog import UMLDialog, UMLDialogType + + self.applicationDiagram = UMLDialog( + UMLDialogType.APPLICATION_DIAGRAM, self, self.parent(), noModules=not res + ) + self.applicationDiagram.show() + + def __loadDiagram(self): + """ + Private slot to load a diagram from file. + """ + from Graphics.UMLDialog import UMLDialog, UMLDialogType + + self.loadedDiagram = None + loadedDiagram = UMLDialog(UMLDialogType.NO_DIAGRAM, self, parent=self.parent()) + if loadedDiagram.load(): + self.loadedDiagram = loadedDiagram + self.loadedDiagram.show(fromFile=True) + + ######################################################################### + ## Below is the interface to the VCS monitor thread + ######################################################################### + + def setStatusMonitorInterval(self, interval): + """ + Public method to se the interval of the VCS status monitor thread. + + @param interval status monitor interval in seconds (integer) + """ + if self.vcs is not None: + self.vcs.setStatusMonitorInterval(interval, self) + + def getStatusMonitorInterval(self): + """ + Public method to get the monitor interval. + + @return interval in seconds (integer) + """ + if self.vcs is not None: + return self.vcs.getStatusMonitorInterval() + else: + return 0 + + def setStatusMonitorAutoUpdate(self, auto): + """ + Public method to enable the auto update function. + + @param auto status of the auto update function (boolean) + """ + if self.vcs is not None: + self.vcs.setStatusMonitorAutoUpdate(auto) + + def getStatusMonitorAutoUpdate(self): + """ + Public method to retrieve the status of the auto update function. + + @return status of the auto update function (boolean) + """ + if self.vcs is not None: + return self.vcs.getStatusMonitorAutoUpdate() + else: + return False + + def checkVCSStatus(self): + """ + Public method to wake up the VCS status monitor thread. + """ + if self.vcs is not None: + self.vcs.checkVCSStatus() + + def clearStatusMonitorCachedState(self, name): + """ + Public method to clear the cached VCS state of a file/directory. + + @param name name of the entry to be cleared (string) + """ + if self.vcs is not None: + self.vcs.clearStatusMonitorCachedState(name) + + def startStatusMonitor(self): + """ + Public method to start the VCS status monitor thread. + """ + if self.vcs is not None: + self.vcs.startStatusMonitor(self) + + def stopStatusMonitor(self): + """ + Public method to stop the VCS status monitor thread. + """ + if self.vcs is not None: + self.vcs.stopStatusMonitor() + + ######################################################################### + ## Below are the plugin development related methods + ######################################################################### + + def __pluginVersionToTuple(self, versionStr): + """ + Private method to convert a plug-in version string into a version + tuple. + + @param versionStr version string to be converted + @type str + @return version info as a tuple + @rtype tuple of int and str + """ + vParts = [] + if "-" in versionStr: + versionStr, additional = versionStr.split("-", 1) + else: + additional = "" + for part in versionStr.split("."): + try: + vParts.append(int(part)) + except ValueError: + vParts.append(part) + + if additional: + vParts.append(additional) + + return tuple(vParts) + + def __pluginCreatePkgList(self): + """ + Private slot to create a PKGLIST file needed for archive file creation. + """ + pkglist = os.path.join(self.ppath, "PKGLIST") + if os.path.exists(pkglist): + res = EricMessageBox.yesNo( + self.ui, + self.tr("Create Package List"), + self.tr( + "<p>The file <b>PKGLIST</b> already" + " exists.</p><p>Overwrite it?</p>" + ), + icon=EricMessageBox.Warning, + ) + if not res: + return # don't overwrite + + # build the list of entries + lst_ = [] + for key in [ + "SOURCES", + "FORMS", + "RESOURCES", + "TRANSLATIONS", + "INTERFACES", + "PROTOCOLS", + "OTHERS", + ]: + lst_.extend(self.pdata[key]) + lst = [] + for entry in lst_: + if os.path.isdir(self.getAbsolutePath(entry)): + lst.extend( + [ + self.getRelativePath(p) + for p in Utilities.direntries(self.getAbsolutePath(entry), True) + ] + ) + continue + else: + lst.append(entry) + lst.sort() + if "PKGLIST" in lst: + lst.remove("PKGLIST") + + # build the header to indicate a freshly generated list + header = [ + ";", + "; initial_list (REMOVE THIS LINE WHEN DONE)", + ";", + " ", + ] + + # write the file + try: + newline = None if self.pdata["EOL"] == 0 else self.getEolString() + with open(pkglist, "w", encoding="utf-8", newline=newline) as pkglistFile: + pkglistFile.write("\n".join(header) + "\n") + pkglistFile.write( + "\n".join([Utilities.fromNativeSeparators(f) for f in lst]) + ) + pkglistFile.write("\n") + # ensure the file ends with an empty line + except OSError as why: + EricMessageBox.critical( + self.ui, + self.tr("Create Package List"), + self.tr( + """<p>The file <b>PKGLIST</b> could not be created.</p>""" + """<p>Reason: {0}</p>""" + ).format(str(why)), + ) + return + + if "PKGLIST" not in self.pdata["OTHERS"]: + self.appendFile("PKGLIST") + + @pyqtSlot() + def __pluginCreateArchives(self, snapshot=False): + """ + Private slot to create eric plugin archives. + + @param snapshot flag indicating snapshot archives (boolean) + """ + if not self.pdata["MAINSCRIPT"]: + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """The project does not have a main script defined. """ + """Aborting...""" + ), + ) + return + + selectedLists = [] + pkglists = [ + os.path.basename(f) for f in glob.glob(os.path.join(self.ppath, "PKGLIST*")) + ] + if len(pkglists) == 1: + selectedLists = [os.path.join(self.ppath, pkglists[0])] + elif len(pkglists) > 1: + dlg = EricListSelectionDialog( + sorted(pkglists), + title=self.tr("Create Plugin Archive"), + message=self.tr("Select package lists:"), + checkBoxSelection=True, + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + selectedLists = [ + os.path.join(self.ppath, s) for s in dlg.getSelection() + ] + else: + return + + if not selectedLists: + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """<p>No package list files (PKGLIST*) available or""" + """ selected. Aborting...</p>""" + ), + ) + return + + progress = EricProgressDialog( + self.tr("Creating plugin archives..."), + self.tr("Abort"), + 0, + len(selectedLists), + self.tr("%v/%m Archives"), + self.ui, + ) + progress.setMinimumDuration(0) + progress.setWindowTitle(self.tr("Create Plugin Archives")) + errors = 0 + for count, pkglist in enumerate(selectedLists): + progress.setValue(count) + if progress.wasCanceled(): + break + + try: + with open(pkglist, "r", encoding="utf-8") as pkglistFile: + names = pkglistFile.read() + except OSError as why: + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """<p>The file <b>{0}</b> could not be read.</p>""" + """<p>Reason: {1}</p>""" + ).format(os.path.basename(pkglist), str(why)), + ) + errors += 1 + continue + + lines = names.splitlines() + archiveName = "" + archiveVersion = "" + names = [] + listOK = True + for line in lines: + if line.startswith(";"): + line = line[1:].strip() + # it's a comment possibly containing a directive + # supported directives are: + # - archive_name= defines the name of the archive + # - archive_version= defines the version of the archive + if line.startswith("archive_name="): + archiveName = line.split("=")[1] + elif line.startswith("archive_version="): + archiveVersion = line.split("=")[1] + elif line.startswith("initial_list "): + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """<p>The file <b>{0}</b> is not ready yet.""" + """</p><p>Please rework it and delete the""" + """'; initial_list' line of the header.""" + """</p>""" + ).format(os.path.basename(pkglist)), + ) + errors += 1 + listOK = False + break + elif line.strip(): + names.append(line.strip()) + + if not listOK: + continue + + names = sorted(names) + archive = ( + os.path.join(self.ppath, archiveName) + if archiveName + else os.path.join( + self.ppath, self.pdata["MAINSCRIPT"].replace(".py", ".zip") + ) + ) + try: + archiveFile = zipfile.ZipFile(archive, "w") + except OSError as why: + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """<p>The eric plugin archive file <b>{0}</b>""" + """ could not be created.</p>""" + """<p>Reason: {1}</p>""" + ).format(archive, str(why)), + ) + errors += 1 + continue + + for name in names: + if name: + try: + self.__createZipDirEntries(os.path.split(name)[0], archiveFile) + if snapshot and name == self.pdata["MAINSCRIPT"]: + snapshotSource, version = self.__createSnapshotSource( + os.path.join(self.ppath, self.pdata["MAINSCRIPT"]) + ) + archiveFile.writestr(name, snapshotSource) + else: + archiveFile.write(os.path.join(self.ppath, name), name) + if name == self.pdata["MAINSCRIPT"]: + version = self.__pluginExtractVersion( + os.path.join(self.ppath, self.pdata["MAINSCRIPT"]) + ) + if archiveVersion and ( + self.__pluginVersionToTuple(version) + < self.__pluginVersionToTuple(archiveVersion) + ): + version = archiveVersion + except OSError as why: + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """<p>The file <b>{0}</b> could not be""" + """ stored in the archive. Ignoring it.</p>""" + """<p>Reason: {1}</p>""" + ).format(os.path.join(self.ppath, name), str(why)), + ) + archiveFile.writestr("VERSION", version.encode("utf-8")) + archiveFile.close() + + if archive not in self.pdata["OTHERS"]: + self.appendFile(archive) + + progress.setValue(len(selectedLists)) + + if errors: + self.ui.showNotification( + UI.PixmapCache.getPixmap("pluginArchive48"), + self.tr("Create Plugin Archive"), + self.tr( + "<p>The eric plugin archive files were " + "created with some errors.</p>" + ), + kind=NotificationTypes.CRITICAL, + timeout=0, + ) + else: + self.ui.showNotification( + UI.PixmapCache.getPixmap("pluginArchive48"), + self.tr("Create Plugin Archive"), + self.tr( + "<p>The eric plugin archive files were " "created successfully.</p>" + ), + ) + + def __pluginCreateSnapshotArchives(self): + """ + Private slot to create eric plugin archive snapshot releases. + """ + self.__pluginCreateArchives(True) + + def __createZipDirEntries(self, path, zipFile): + """ + Private method to create dir entries in the zip file. + + @param path name of the directory entry to create (string) + @param zipFile open ZipFile object (zipfile.ZipFile) + """ + if path in ("", "/", "\\"): + return + + if not path.endswith("/") and not path.endswith("\\"): + path = "{0}/".format(path) + + if path not in zipFile.namelist(): + self.__createZipDirEntries(os.path.split(path[:-1])[0], zipFile) + zipFile.writestr(path, b"") + + def __createSnapshotSource(self, filename): + """ + Private method to create a snapshot plugin version. + + The version entry in the plugin module is modified to signify + a snapshot version. This method appends the string "-snapshot-" + and date indicator to the version string. + + @param filename name of the plugin file to modify (string) + @return modified source (bytes), snapshot version string (string) + """ + try: + sourcelines, encoding = Utilities.readEncodedFile(filename) + sourcelines = sourcelines.splitlines(True) + except (OSError, UnicodeError) as why: + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """<p>The plugin file <b>{0}</b> could """ + """not be read.</p>""" + """<p>Reason: {1}</p>""" + ).format(filename, str(why)), + ) + return b"", "" + + lineno = 0 + while lineno < len(sourcelines): + if sourcelines[lineno].startswith("version = "): + # found the line to modify + datestr = time.strftime("%Y%m%d") + lineend = sourcelines[lineno].replace(sourcelines[lineno].rstrip(), "") + sversion = "{0}-snapshot-{1}".format( + sourcelines[lineno].replace("version = ", "").strip()[1:-1], datestr + ) + sourcelines[lineno] = '{0} + "-snapshot-{1}"{2}'.format( + sourcelines[lineno].rstrip(), datestr, lineend + ) + break + + lineno += 1 + + source = Utilities.encode("".join(sourcelines), encoding)[0] + return source, sversion + + def __pluginExtractVersion(self, filename): + """ + Private method to extract the version number entry. + + @param filename name of the plugin file (string) + @return version string (string) + """ + version = "0.0.0" + try: + sourcelines = Utilities.readEncodedFile(filename)[0] + sourcelines = sourcelines.splitlines(True) + except (OSError, UnicodeError) as why: + EricMessageBox.critical( + self.ui, + self.tr("Create Plugin Archive"), + self.tr( + """<p>The plugin file <b>{0}</b> could """ + """not be read.</p> <p>Reason: {1}</p>""" + ).format(filename, str(why)), + ) + return "" + + for sourceline in sourcelines: + if sourceline.startswith("version = "): + version = ( + sourceline.replace("version = ", "") + .strip() + .replace('"', "") + .replace("'", "") + ) + break + + return version + + ######################################################################### + ## Below are methods implementing the 'make' support + ######################################################################### + + def __showContextMenuMake(self): + """ + Private slot called before the make menu is shown. + """ + self.showMenu.emit("Make", self.makeMenu) + + def hasDefaultMakeParameters(self): + """ + Public method to test, if the project contains the default make + parameters. + + @return flag indicating default parameter set + @rtype bool + """ + return self.pdata["MAKEPARAMS"] == { + "MakeEnabled": False, + "MakeExecutable": "", + "MakeFile": "", + "MakeTarget": "", + "MakeParameters": "", + "MakeTestOnly": True, + } + + def isMakeEnabled(self): + """ + Public method to test, if make is enabled for the project. + + @return flag indicating enabled make support + @rtype bool + """ + return self.pdata["MAKEPARAMS"]["MakeEnabled"] + + @pyqtSlot() + def executeMake(self): + """ + Public slot to execute a project specific make run (auto-run) + (execute or question). + """ + self.__executeMake( + questionOnly=self.pdata["MAKEPARAMS"]["MakeTestOnly"], interactive=False + ) + + @pyqtSlot() + def __executeMake(self, questionOnly=False, interactive=True): + """ + Private method to execute a project specific make run. + + @param questionOnly flag indicating to ask make for changes only + @type bool + @param interactive flag indicating an interactive invocation (i.e. + through a menu action) + @type bool + """ + if ( + not self.pdata["MAKEPARAMS"]["MakeEnabled"] + or self.__makeProcess is not None + ): + return + + prog = ( + self.pdata["MAKEPARAMS"]["MakeExecutable"] + if self.pdata["MAKEPARAMS"]["MakeExecutable"] + else Project.DefaultMake + ) + + args = [] + if self.pdata["MAKEPARAMS"]["MakeParameters"]: + args.extend( + Utilities.parseOptionString(self.pdata["MAKEPARAMS"]["MakeParameters"]) + ) + + if self.pdata["MAKEPARAMS"]["MakeFile"]: + args.append("--makefile={0}".format(self.pdata["MAKEPARAMS"]["MakeFile"])) + + if questionOnly: + args.append("--question") + + if self.pdata["MAKEPARAMS"]["MakeTarget"]: + args.append(self.pdata["MAKEPARAMS"]["MakeTarget"]) + + self.__makeProcess = QProcess(self) + self.__makeProcess.readyReadStandardOutput.connect(self.__makeReadStdOut) + self.__makeProcess.readyReadStandardError.connect(self.__makeReadStdErr) + self.__makeProcess.finished.connect( + lambda exitCode, exitStatus: self.__makeFinished( + exitCode, exitStatus, questionOnly, interactive + ) + ) + self.__makeProcess.setWorkingDirectory(self.getProjectPath()) + self.__makeProcess.start(prog, args) + + if not self.__makeProcess.waitForStarted(): + EricMessageBox.critical( + self.ui, + self.tr("Execute Make"), + self.tr("""The make process did not start."""), + ) + + self.__cleanupMake() + + @pyqtSlot() + def __makeReadStdOut(self): + """ + Private slot to process process output received via stdout. + """ + if self.__makeProcess is not None: + output = str( + self.__makeProcess.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + "replace", + ) + self.appendStdout.emit(output) + + @pyqtSlot() + def __makeReadStdErr(self): + """ + Private slot to process process output received via stderr. + """ + if self.__makeProcess is not None: + error = str( + self.__makeProcess.readAllStandardError(), + Preferences.getSystem("IOEncoding"), + "replace", + ) + self.appendStderr.emit(error) + + def __makeFinished(self, exitCode, exitStatus, questionOnly, interactive=True): + """ + Private slot handling the make process finished signal. + + @param exitCode exit code of the make process + @type int + @param exitStatus exit status of the make process + @type QProcess.ExitStatus + @param questionOnly flag indicating a test only run + @type bool + @param interactive flag indicating an interactive invocation (i.e. + through a menu action) + @type bool + """ + if exitStatus == QProcess.ExitStatus.CrashExit: + EricMessageBox.critical( + self.ui, + self.tr("Execute Make"), + self.tr("""The make process crashed."""), + ) + else: + if questionOnly and exitCode == 1: + # a rebuild is needed + title = self.tr("Test for Changes") + + if self.pdata["MAKEPARAMS"]["MakeTarget"]: + message = self.tr( + """<p>There are changes that require the configured""" + """ make target <b>{0}</b> to be rebuilt.</p>""" + ).format(self.pdata["MAKEPARAMS"]["MakeTarget"]) + else: + message = self.tr( + """<p>There are changes that require the default""" + """ make target to be rebuilt.</p>""" + ) + + self.ui.showNotification( + UI.PixmapCache.getPixmap("makefile48"), + title, + message, + kind=NotificationTypes.WARNING, + timeout=0, + ) + elif exitCode > 1: + EricMessageBox.critical( + self.ui, + self.tr("Execute Make"), + self.tr("""The makefile contains errors."""), + ) + + self.__cleanupMake() + + def __cleanupMake(self): + """ + Private method to clean up make related stuff. + """ + self.__makeProcess.readyReadStandardOutput.disconnect() + self.__makeProcess.readyReadStandardError.disconnect() + self.__makeProcess.finished.disconnect() + self.__makeProcess.deleteLater() + self.__makeProcess = None + + ######################################################################### + ## Below are methods implementing some 'IDL' support functions + ######################################################################### + + def hasDefaultIdlCompilerParameters(self): + """ + Public method to test, if the project contains the default IDL compiler + parameters. + + @return flag indicating default parameter set + @rtype bool + """ + return self.pdata["IDLPARAMS"] == { + "IncludeDirs": [], + "DefinedNames": [], + "UndefinedNames": [], + } + + ######################################################################### + ## Below are methods implementing some 'UIC' support functions + ######################################################################### + + def hasDefaultUicCompilerParameters(self): + """ + Public method to test, if the project contains the default uic compiler + parameters. + + @return flag indicating default parameter set + @rtype bool + """ + return self.pdata["UICPARAMS"] == { + "Package": "", + "RcSuffix": "", + "PackagesRoot": "", + } + + def getUicParameter(self, name): + """ + Public method to get a named uic related parameter. + + @param name name of the parameter + @type str + @return value of the given parameter + @rtype any, None in case on non-existence + """ + if name in self.pdata["UICPARAMS"]: + return self.pdata["UICPARAMS"][name] + else: + return None + + ######################################################################### + ## Below are methods implementing some 'RCC' support functions + ######################################################################### + + def hasDefaultRccCompilerParameters(self): + """ + Public method to test, if the project contains the default rcc compiler + parameters. + + @return flag indicating default parameter set + @rtype bool + """ + return self.pdata["RCCPARAMS"] == self.getDefaultRccCompilerParameters() + + def getDefaultRccCompilerParameters(self): + """ + Public method to get the default rcc compiler parameters. + + @return dictionary containing the default rcc compiler parameters + @rtype dict + """ + return { + "CompressionThreshold": 70, # default value + "CompressLevel": 0, # use zlib default + "CompressionDisable": False, + "PathPrefix": "", + } + + ######################################################################### + ## Below are methods implementing some 'docstring' support functions + ######################################################################### + + def hasDefaultDocstringParameter(self): + """ + Public method to test, if the project contains the default docstring + parameter. + + @return flag indicating default parameter + @rtype bool + """ + return self.pdata["DOCSTRING"] == "" + + def getDocstringType(self): + """ + Public method to get the configured docstring style. + + @return configured docstring style + @rtype str + """ + return self.pdata["DOCSTRING"] + + ######################################################################### + ## Below are methods implementing the 'SBOM' support + ######################################################################### + + def __showContextMenuOthers(self): + """ + Private slot called before the 'Other Tools' menu is shown. + """ + self.showMenu.emit("OtherTools", self.othersMenu) + + @pyqtSlot() + def __createSBOMFile(self): + """ + Private slot to create a SBOM file of the project dependencies. + """ + import CycloneDXInterface + + CycloneDXInterface.createCycloneDXFile("<project>") + + ######################################################################### + ## Below are methods implementing the 'Code Formatting' support + ######################################################################### + + def __showContextMenuFormat(self): + """ + Private slot called before the 'Code Formatting' menu is shown. + """ + self.showMenu.emit("Formatting", self.othersMenu) + + @pyqtSlot() + def __aboutBlack(self): + """ + Private slot to show some information about the installed 'Black' tool. + """ + import black + + EricMessageBox.information( + None, + self.tr("About Black"), + self.tr( + """<p><b>Black Version {0}</b></p>""" + """<p><i>Black</i> is the uncompromising Python code""" + """ formatter.</p>""" + ).format(black.__version__), + ) + + def __performFormatWithBlack(self, action): + """ + Private method to format the project sources using the 'Black' tool. + + Following actions are supported. + <ul> + <li>BlackFormattingAction.Format - the code reformatting is performed</li> + <li>BlackFormattingAction.Check - a check is performed, if code formatting + is necessary</li> + <li>BlackFormattingAction.Diff - a unified diff of potential code formatting + changes is generated</li> + </ul> + + @param action formatting operation to be performed + @type BlackFormattingAction + """ + from CodeFormatting.BlackConfigurationDialog import BlackConfigurationDialog + from CodeFormatting.BlackFormattingDialog import BlackFormattingDialog + + if ericApp().getObject("ViewManager").checkAllDirty(): + dlg = BlackConfigurationDialog(withProject=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + config = dlg.getConfiguration() + + formattingDialog = BlackFormattingDialog( + config, + self.getProjectFiles("SOURCES", normalized=True), + project=self, + action=action, + ) + formattingDialog.exec() + + +# +# eflag: noqa = M601