diff -r 0a74c1efab70 -r 0daf79d65080 Project/Project.py --- a/Project/Project.py Mon Apr 02 12:04:56 2018 +0200 +++ b/Project/Project.py Tue May 01 12:03:52 2018 +0200 @@ -22,7 +22,7 @@ import zipfile from PyQt5.QtCore import pyqtSlot, QFile, QFileInfo, pyqtSignal, \ - QCryptographicHash, QIODevice, QByteArray, QObject, Qt + QCryptographicHash, QIODevice, QByteArray, QObject, Qt, QProcess from PyQt5.QtGui import QCursor, QKeySequence from PyQt5.QtWidgets import QLineEdit, QToolBar, QDialog, QInputDialog, \ QApplication, QMenu, QAction @@ -101,6 +101,10 @@ @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(int) projectLanguageAdded = pyqtSignal(str) @@ -139,9 +143,14 @@ 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 @@ -170,6 +179,7 @@ } self.vcsMenu = None + self.__makeProcess = None self.__initProjectTypes() @@ -459,6 +469,14 @@ "PACKAGERSPARMS": {}, "DOCUMENTATIONPARMS": {}, "OTHERTOOLSPARMS": {}, + "MAKEPARAMS": { + "MakeEnabled": False, + "MakeExecutable": "", + "MakeFile": "", + "MakeTarget": "", + "MakeParameters": "", + "MakeTestOnly": True, + }, "EOL": -1, } @@ -536,6 +554,9 @@ "README": "OTHERS", "README.*": "OTHERS", "*.e4p": "OTHERS", + "GNUmakefile": "OTHERS", + "makefile": "OTHERS", + "Makefile": "OTHERS", } # Sources @@ -1401,13 +1422,14 @@ fn = os.path.join(self.ppath, langFile) if os.path.exists(fn): s2t(fn) - except EnvironmentError: + except EnvironmentError as err: E5MessageBox.critical( self.ui, self.tr("Delete translation"), self.tr( "<p>The selected translation file <b>{0}</b> could not be" - " deleted.</p>").format(langFile)) + " deleted.</p><p>Reason: {1}</p>").format( + langFile, str(err))) return self.removeLanguageFile(langFile) @@ -1422,13 +1444,14 @@ fn = os.path.join(self.ppath, qmFile) if os.path.exists(fn): s2t(fn) - except EnvironmentError: + except EnvironmentError as err: E5MessageBox.critical( self.ui, self.tr("Delete translation"), self.tr( "<p>The selected translation file <b>{0}</b> could" - " not be deleted.</p>").format(qmFile)) + " not be deleted.</p><p>Reason: {1}</p>").format( + qmFile, str(err))) return def appendFile(self, fn, isSourceFile=False, updateModel=True): @@ -2119,13 +2142,14 @@ "__pycache__", "{0}.*{1}".format(tail, ext)) for f in glob.glob(pat): s2t(f) - except EnvironmentError: + except EnvironmentError as err: E5MessageBox.critical( self.ui, self.tr("Delete file"), self.tr( "<p>The selected file <b>{0}</b> could not be" - " deleted.</p>").format(fn)) + " deleted.</p><p>Reason: {1}</p>").format( + fn, str(err))) return False self.removeFile(fn) @@ -2148,13 +2172,13 @@ send2trash(dn) except ImportError: shutil.rmtree(dn, True) - except EnvironmentError: + except EnvironmentError as err: E5MessageBox.critical( self.ui, self.tr("Delete directory"), self.tr( "<p>The selected directory <b>{0}</b> could not be" - " deleted.</p>").format(dn)) + " deleted.</p><p>Reason: {1}</p>").format(dn, str(err))) return False self.removeDirectory(dn) @@ -2218,6 +2242,10 @@ self.pdata["PROJECTTYPE"] in ["E6Plugin"]) self.addLanguageAct.setEnabled( bool(self.pdata["TRANSLATIONPATTERN"])) + self.makeGrp.setEnabled( + self.pdata["MAKEPARAMS"]["MakeEnabled"]) + self.menuMakeAct.setEnabled( + self.pdata["MAKEPARAMS"]["MakeEnabled"]) self.projectAboutToBeCreated.emit() @@ -2241,6 +2269,7 @@ .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 \ @@ -2249,6 +2278,7 @@ f = open(fn, "w", encoding="utf-8") f.close() 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"]): @@ -2259,6 +2289,18 @@ f = open(ms, "w") f.close() 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) + f = open(mf, "w") + f.close() + self.appendFile(mf) + tpd = os.path.join(self.ppath, self.translationsRoot) if not self.translationsRoot.endswith(os.sep): tpd = os.path.dirname(tpd) @@ -2305,11 +2347,32 @@ self.tr( "<p>The mainscript <b>{0}</b> could not" " be created.<br/>Reason: {1}</p>") - .format(self.ppath, str(err))) - self.appendFile(ms) + .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: + f = open(mf, "w") + f.close() + except EnvironmentError as err: + E5MessageBox.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 = E5MessageBox.yesNo( self.ui, @@ -2567,6 +2630,27 @@ 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: + f = open(mf, "w") + f.close() + except EnvironmentError as err: + E5MessageBox.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() @@ -2811,6 +2895,10 @@ self.pdata["PROJECTTYPE"] in ["E6Plugin"]) self.addLanguageAct.setEnabled( bool(self.pdata["TRANSLATIONPATTERN"])) + self.makeGrp.setEnabled( + self.pdata["MAKEPARAMS"]["MakeEnabled"]) + self.menuMakeAct.setEnabled( + self.pdata["MAKEPARAMS"]["MakeEnabled"]) self.__model.projectOpened() self.projectOpenedHooks.emit() @@ -3008,13 +3096,21 @@ not noSave: self.__writeDebugProperties(True) - # now save all open modified files of the project vm = e5App().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) - + success &= vm.closeWindow(fn, ignoreDirty=True) if not success: return False @@ -3035,7 +3131,7 @@ self.vcs = None e5App().getObject("PluginManager").deactivateVcsPlugins() - # now close all project related windows + # now close all project related tool windows self.__closeAllWindows() self.__initData() @@ -3057,6 +3153,8 @@ self.menuApidocAct.setEnabled(False) self.menuPackagersAct.setEnabled(False) self.pluginGrp.setEnabled(False) + self.makeGrp.setEnabled(False) + self.menuMakeAct.setEnabled(False) self.__model.projectClosed() self.projectClosedHooks.emit() @@ -4031,6 +4129,37 @@ self.__pluginCreateSnapshotArchives) self.actions.append(self.pluginSArchiveAct) + self.makeGrp = createActionGroup(self) + + self.makeExecuteAct = E5Action( + 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 = E5Action( + 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) + self.closeAct.setEnabled(False) self.saveAct.setEnabled(False) self.saveasAct.setEnabled(False) @@ -4064,7 +4193,7 @@ self.apidocMenu.setTearOffEnabled(True) self.debuggerMenu = QMenu(self.tr('Debugger'), menu) self.packagersMenu = QMenu(self.tr('Pac&kagers'), menu) - self.packagersMenu.setTearOffEnabled(True) + self.makeMenu = QMenu(self.tr('Make'), menu) self.__menus = { "Main": menu, @@ -4077,6 +4206,7 @@ "Apidoc": self.apidocMenu, "Debugger": self.debuggerMenu, "Packagers": self.packagersMenu, + "Make": self.makeMenu, } # connect the aboutToShow signals @@ -4090,6 +4220,7 @@ self.packagersMenu.aboutToShow.connect(self.__showContextMenuPackagers) self.sessionMenu.aboutToShow.connect(self.__showContextMenuSession) self.debuggerMenu.aboutToShow.connect(self.__showContextMenuDebugger) + self.makeMenu.aboutToShow.connect(self.__showContextMenuMake) menu.aboutToShow.connect(self.__showMenu) # build the show menu @@ -4113,9 +4244,15 @@ 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 main menu menu.setTearOffEnabled(True) menu.addActions(self.actGrp1.actions()) @@ -4131,6 +4268,8 @@ menu.addSeparator() menu.addActions(self.actGrp2.actions()) menu.addSeparator() + self.menuMakeAct = menu.addMenu(self.makeMenu) + menu.addSeparator() self.menuDiagramAct = menu.addMenu(self.graphicsMenu) menu.addSeparator() self.menuCheckAct = menu.addMenu(self.checksMenu) @@ -4155,6 +4294,7 @@ self.menuDebuggerAct.setEnabled(False) self.menuApidocAct.setEnabled(False) self.menuPackagersAct.setEnabled(False) + self.menuMakeAct.setEnabled(False) self.menu = menu return menu @@ -5258,3 +5398,185 @@ 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 + + if self.pdata["MAKEPARAMS"]["MakeExecutable"]: + prog = self.pdata["MAKEPARAMS"]["MakeExecutable"] + else: + prog = 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(): + E5MessageBox.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.CrashExit: + E5MessageBox.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>""") + + if self.ui.notificationsEnabled() and not interactive: + self.ui.showNotification( + UI.PixmapCache.getPixmap("makefile48.png"), + title, + message) + else: + E5MessageBox.information(self.ui, title, message) + elif exitCode > 1: + E5MessageBox.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