Sat, 21 Nov 2020 17:50:57 +0100
Done implementing pybabel translations support.
--- a/PluginFlask.e4p Thu Nov 19 20:19:55 2020 +0100 +++ b/PluginFlask.e4p Sat Nov 21 17:50:57 2020 +0100 @@ -21,6 +21,7 @@ <Source>ProjectFlask/FlaskBabelDetector.py</Source> <Source>ProjectFlask/FlaskCommandDialog.py</Source> <Source>ProjectFlask/Project.py</Source> + <Source>ProjectFlask/PyBabelCommandDialog.py</Source> <Source>ProjectFlask/PyBabelConfigDialog.py</Source> <Source>ProjectFlask/RoutesDialog.py</Source> <Source>ProjectFlask/RunServerDialog.py</Source>
--- a/PluginProjectFlask.py Thu Nov 19 20:19:55 2020 +0100 +++ b/PluginProjectFlask.py Sat Nov 21 17:50:57 2020 +0100 @@ -427,7 +427,7 @@ "Flask", self.tr("Pyramid"), self.fileTypesCallback, lexerAssociationCallback=self.lexerAssociationCallback, -# binaryTranslationsCallback=self.binaryTranslationsCallback, + binaryTranslationsCallback=self.binaryTranslationsCallback, progLanguages=self.__supportedVariants[:]) def getMenu(self, name):
--- a/ProjectFlask/FlaskCommandDialog.py Thu Nov 19 20:19:55 2020 +0100 +++ b/ProjectFlask/FlaskCommandDialog.py Sat Nov 21 17:50:57 2020 +0100 @@ -19,30 +19,39 @@ """ Class implementing a dialog to run a flask command and show its output. """ - def __init__(self, project, parent=None): + def __init__(self, project, title="", msgSuccess="", msgError="", + parent=None): """ Constructor @param project reference to the project object @type Project + @param title window title of the dialog + @type str + @param msgSuccess success message to be shown + @type str + @param msgError message to be shown on error + @type str @param parent reference to the parent widget @type QWidget """ super(FlaskCommandDialog, self).__init__(parent) self.setupUi(self) + if title: + self.setWindowTitle(title) + self.__project = project + self.__successMessage = msgSuccess + self.__errorMessage = msgError self.__process = None - self.successMessage = "" - self.errorMessage = "" - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) - def startFlaskCommand(self, command, args=None): + def startCommand(self, command, args=None): """ Public method to start a flask command and show its output. @@ -89,61 +98,6 @@ return ok - def startBabelCommand(self, command, args, title, msgSuccess="", - msgError=""): - """ - Public method to start a pybabel command and show its output. - - @param command pybabel command to be run - @type str - @param args list of command line arguments for the command - @type list of str - @param title window title of the dialog - @type str - @param msgSuccess success message to be shown - @type str - @param msgError message to be shown on error - @type str - @return flag indicating a successful start - @rtype bool - """ - self.setWindowTitle(title) - - self.successMessage = msgSuccess - self.errorMessage = msgError - - workdir, _ = self.__project.getApplication() - babelCommand = self.__project.getBabelCommand() - - self.__process = QProcess() - self.__process.setWorkingDirectory(workdir) - self.__process.setProcessChannelMode(QProcess.MergedChannels) - - self.__process.readyReadStandardOutput.connect(self.__readStdOut) - self.__process.finished.connect(self.__processFinished) - - self.outputEdit.clear() - - babelArgs = [command] - if args: - babelArgs += args - - self.__process.start(babelCommand, babelArgs) - ok = self.__process.waitForStarted(10000) - if not ok: - E5MessageBox.critical( - None, - self.tr("Execute PyBabel Command"), - self.tr("""The pybabel process could not be started.""")) - else: - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) - self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setFocus( - Qt.OtherFocusReason) - - return ok - def closeEvent(self, evt): """ Protected method handling the close event of the dialog. @@ -181,10 +135,10 @@ self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) - if normal and self.successMessage: - self.outputEdit.insertPlainText(self.successMessage) - elif not normal and self.errorMessage: - self.outputEdit.insertPlainText(self.errorMessage) + if normal and self.__successMessage: + self.outputEdit.insertPlainText(self.__successMessage) + elif not normal and self.__errorMessage: + self.outputEdit.insertPlainText(self.__errorMessage) @pyqtSlot() def __cancelProcess(self):
--- a/ProjectFlask/Project.py Thu Nov 19 20:19:55 2020 +0100 +++ b/ProjectFlask/Project.py Sat Nov 21 17:50:57 2020 +0100 @@ -8,6 +8,7 @@ """ import os +import re from PyQt5.QtCore import ( pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer @@ -24,8 +25,11 @@ import Utilities from .FlaskCommandDialog import FlaskCommandDialog +from .PyBabelCommandDialog import PyBabelCommandDialog +# TODO: move PyBabel related code to a separate package (FlaskBabelExtension) +# TODO: move database related code to a separate package (FlaskMigrateExtension) class Project(QObject): """ Class implementing the Flask project support. @@ -67,6 +71,11 @@ "flask": "", "werkzeug": "", } + + self.__capabilities = { + "pybabel": False, + "migrate": False, + } def initActions(self): """ @@ -178,7 +187,7 @@ self.actions.append(self.initDatabaseAct) ################################## - ## database action below ## + ## pybabel action below ## ################################## self.pybabelConfigAct = E5Action( @@ -243,19 +252,18 @@ menu = QMenu(self.tr('&Flask'), self.__ui) menu.setTearOffEnabled(True) - menu.addSection("flask run") menu.addAction(self.runServerAct) menu.addAction(self.runDevServerAct) menu.addAction(self.askForServerOptionsAct) - menu.addSection("flask shell") + menu.addSeparator() menu.addAction(self.runPythonShellAct) - menu.addSection("flask routes") + menu.addSeparator() menu.addAction(self.showRoutesAct) - menu.addSection("flask init-db") + menu.addSeparator() menu.addAction(self.initDatabaseAct) - menu.addSection(self.tr("Translations")) + menu.addSeparator() menu.addAction(self.pybabelConfigAct) - menu.addSection(self.tr("Various")) + menu.addSeparator() menu.addAction(self.documentationAct) menu.addSeparator() menu.addAction(self.aboutFlaskAct) @@ -308,13 +316,16 @@ Public method to add our hook methods. """ if self.__e5project.getProjectType() == "Flask": + # TODO: add some methods for standard templates ## self.__formsBrowser = ( ## e5App().getObject("ProjectBrowser") ## .getProjectBrowser("forms")) ## self.__formsBrowser.addHookMethodAndMenuEntry( ## "newForm", self.newForm, self.tr("New template...")) ## - if self.flaskBabelAvailable(): + self.__determineCapabilities() + + if self.__capabilities["pybabel"]: self.__e5project.projectLanguageAddedByCode.connect( self.__projectLanguageAdded) self.__translationsBrowser = ( @@ -333,8 +344,15 @@ "generateAll", self.updateCatalogs, self.tr("Update All Catalogs")) self.__translationsBrowser.addHookMethodAndMenuEntry( + "generateAllWithObsolete", self.updateCatalogsObsolete, + self.tr("Update All Catalogs (with obsolete)")) + self.__translationsBrowser.addHookMethodAndMenuEntry( "generateSelected", self.updateSelectedCatalogs, self.tr("Update Selected Catalogs")) + self.__translationsBrowser.addHookMethodAndMenuEntry( + "generateSelectedWithObsolete", + self.updateSelectedCatalogsObsolete, + self.tr("Update Selected Catalogs (with obsolete)")) self.__hooksInstalled = True @@ -350,12 +368,22 @@ ## self.__e5project.projectLanguageAddedByCode.disconnect( self.__projectLanguageAdded) - self.__translationsBrowser.removeHookMethod("extractMessages") - self.__translationsBrowser.removeHookMethod("releaseAll") - self.__translationsBrowser.removeHookMethod("releaseSelected") - self.__translationsBrowser.removeHookMethod("generateAll") - self.__translationsBrowser.removeHookMethod("generateSelected") - self.__translationsBrowser.removeHookMethod("open") + self.__translationsBrowser.removeHookMethod( + "extractMessages") + self.__translationsBrowser.removeHookMethod( + "releaseAll") + self.__translationsBrowser.removeHookMethod( + "releaseSelected") + self.__translationsBrowser.removeHookMethod( + "generateAll") + self.__translationsBrowser.removeHookMethod( + "generateAllWithObsolete") + self.__translationsBrowser.removeHookMethod( + "generateSelected") + self.__translationsBrowser.removeHookMethod( + "generateSelectedWithObsolete") + self.__translationsBrowser.removeHookMethod( + "open") self.__translationsBrowser = None self.__hooksInstalled = False @@ -649,8 +677,33 @@ self.__e5project.setData( "PROJECTTYPESPECIFICDATA", category, self.__projectData[category]) + def __determineCapabilities(self): + """ + Private method to determine capabilities provided by supported + extensions. + """ + # 1. support for flask-babel (i.e. pybabel) + self.__capabilities["pybabel"] = self.flaskBabelAvailable() + self.pybabelConfigAct.setEnabled(self.__capabilities["pybabel"]) + + # 2. support for flask-migrate + # TODO: add support for flask-migrate + ################################################################## - ## slots below implement documentation functions + ## slot below implements project specific flask configuration + ################################################################## + + @pyqtSlot() + def __configureFlaskForProject(self): + """ + Private slot to configure the project specific flask parameters. + """ + # TODO: implement the flask project config dialog + # 1. check boxes to override flask-babel and flask-migrate + # 2. support for project specific virtual environment + + ################################################################## + ## slot below implements documentation function ################################################################## def __showDocumentation(self): @@ -759,13 +812,14 @@ dlg.show() self.__routesDialog = dlg + # TODO: replace this by commands made by flask-migrate (flask db ...) @pyqtSlot() def __initDatabase(self): """ Private slot showing the result of the database creation. """ dlg = FlaskCommandDialog(self) - if dlg.startFlaskCommand("init-db"): + if dlg.startCommand("init-db"): dlg.exec() ################################################################## @@ -799,7 +853,6 @@ """ Private slot to show a dialog to edit the pybabel configuration. """ - # TODO: implement this from .PyBabelConfigDialog import PyBabelConfigDialog config = self.getData("pybabel", "") @@ -808,6 +861,12 @@ config = dlg.getConfiguration() self.setData("pybabel", "", config) + self.__e5project.setTranslationPattern(os.path.join( + config["translationsDirectory"], "%language%", "LC_MESSAGES", + "{0}.po".format(config["domain"]) + )) + self.__e5project.setDirty(True) + cfgFileName = self.__e5project.getAbsoluteUniversalPath( config["configFile"]) if not os.path.exists(cfgFileName): @@ -840,6 +899,8 @@ """ Private method to create a template pybabel configuration file. + @param configFile name of the configuration file to be created + @type str @return flag indicating successful configuration file creation @rtype bool """ @@ -878,9 +939,30 @@ ) return False - def __projectLanguageAdded(self, code): - # TODO: implement this with pybabel ... - pass + def __getLocale(self, filename): + """ + Private method to extract the locale out of a file name. + + @param filename name of the file used for extraction + @type str + @return extracted locale + @rtype str or None + """ + if self.__e5project.getTranslationPattern(): + filename = os.path.splitext(filename)[0] + ".po" + + # On Windows, path typically contains backslashes. This leads + # to an invalid search pattern '...\(' because the opening bracket + # will be escaped. + pattern = self.__e5project.getTranslationPattern() + pattern = os.path.normpath(pattern) + pattern = pattern.replace("%language%", "(.*?)") + pattern = pattern.replace('\\', '\\\\') + match = re.search(pattern, filename) + if match is not None: + return match.group(1) + + return None def openPOEditor(self, poFile): """ @@ -907,6 +989,7 @@ """ title = self.tr("Extract messages") if self.__ensurePybabelConfigured(): + workdir = self.getApplication()[0] potFile = self.__e5project.getAbsoluteUniversalPath( self.getData("pybabel", "catalogFile")) @@ -918,41 +1001,251 @@ args = [ "-F", - self.__e5project.getAbsoluteUniversalPath( - self.getData("pybabel", "configFile")) + os.path.relpath( + self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "configFile")), + workdir + ) ] if self.getData("pybabel", "markersList"): for marker in self.getData("pybabel", "markersList"): args += ["-k", marker] args += [ "-o", - potFile, + os.path.relpath(potFile, workdir), "." ] - dlg = FlaskCommandDialog(self) - res = dlg.startBabelCommand( - "extract", args, title, + dlg = PyBabelCommandDialog( + self, title, msgSuccess=self.tr("\nMessages extracted successfully.") ) + res = dlg.startCommand("extract", args, workdir) if res: dlg.exec() self.__e5project.appendFile(potFile) - # TODO: implement this with pybabel ... - pass - def compileCatalogs(self): - # TODO: implement this with pybabel ... - pass + def __projectLanguageAdded(self, code): + """ + Private slot handling the addition of a new language. + + @param code language code of the new language + @type str + """ + title = self.tr( + "Initializing message catalog for '{0}'").format(code) + + if self.__ensurePybabelConfigured(): + workdir = self.getApplication()[0] + langFile = self.__e5project.getAbsoluteUniversalPath( + self.__e5project.getTranslationPattern().replace( + "%language%", code)) + potFile = self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "catalogFile")) + + args = [ + "--domain={0}".format(self.getData("pybabel", "domain")), + "--input-file={0}".format(os.path.relpath(potFile, workdir)), + "--output-file={0}".format(os.path.relpath(langFile, workdir)), + "--locale={0}".format(code), + ] + + dlg = PyBabelCommandDialog( + self, title, + msgSuccess=self.tr( + "\nMessage catalog initialized successfully.") + ) + res = dlg.startCommand("init", args, workdir) + if res: + dlg.exec() + + self.__e5project.appendFile(langFile) + + def compileCatalogs(self, filenames): + """ + Public method to compile the message catalogs. + + @param filenames list of filenames (not used) + @type list of str + """ + title = self.tr("Compiling message catalogs") + + if self.__ensurePybabelConfigured(): + workdir = self.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "translationsDirectory")) + + args = [ + "--domain={0}".format(self.getData("pybabel", "domain")), + "--directory={0}".format( + os.path.relpath(translationsDirectory, workdir)), + "--use-fuzzy", + "--statistics", + ] + + dlg = PyBabelCommandDialog( + self, title, + msgSuccess=self.tr("\nMessage catalogs compiled successfully.") + ) + res = dlg.startCommand("compile", args, workdir) + if res: + dlg.exec() + + for entry in os.walk(translationsDirectory): + for fileName in entry[2]: + fullName = os.path.join(entry[0], fileName) + if fullName.endswith('.mo'): + self.__e5project.appendFile(fullName) - def compileSelectedCatalogs(self): - # TODO: implement this with pybabel ... - pass + def compileSelectedCatalogs(self, filenames): + """ + Public method to update the message catalogs. + + @param filenames list of file names + @type list of str + """ + title = self.tr("Compiling message catalogs") + + locales = {self.__getLocale(f) for f in filenames} + + if len(locales) == 0: + E5MessageBox.warning( + self.__ui, + title, + self.tr('No locales detected. Aborting...')) + return + + if self.__ensurePybabelConfigured(): + workdir = self.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "translationsDirectory")) + + argsList = [] + for loc in locales: + argsList.append([ + "compile", + "--domain={0}".format(self.getData("pybabel", "domain")), + "--directory={0}".format( + os.path.relpath(translationsDirectory, workdir)), + "--use-fuzzy", + "--statistics", + "--locale={0}".format(loc), + ]) + + dlg = PyBabelCommandDialog( + self, title=title, + msgSuccess=self.tr("\nMessage catalogs compiled successfully.") + ) + res = dlg.startBatchCommand(argsList, workdir) + if res: + dlg.exec() + + for entry in os.walk(translationsDirectory): + for fileName in entry[2]: + fullName = os.path.join(entry[0], fileName) + if fullName.endswith('.mo'): + self.__e5project.appendFile(fullName) - def updateCatalogs(self): - # TODO: implement this with pybabel ... - pass + def updateCatalogs(self, filenames, withObsolete=False): + """ + Public method to update the message catalogs. + + @param filenames list of filenames (not used) + @type list of str + @param withObsolete flag indicating to keep obsolete translations + @type bool + """ + title = self.tr("Updating message catalogs") + + if self.__ensurePybabelConfigured(): + workdir = self.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "translationsDirectory")) + potFile = self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "catalogFile")) + + args = [ + "--domain={0}".format(self.getData("pybabel", "domain")), + "--input-file={0}".format(os.path.relpath(potFile, workdir)), + "--output-dir={0}".format( + os.path.relpath(translationsDirectory, workdir)), + ] + if not withObsolete: + args.append("--ignore-obsolete") + + dlg = PyBabelCommandDialog( + self, title, + msgSuccess=self.tr("\nMessage catalogs updated successfully.") + ) + res = dlg.startCommand("update", args, workdir) + if res: + dlg.exec() + + def updateCatalogsObsolete(self, filenames): + """ + Public method to update the message catalogs keeping obsolete + translations. + + @param filenames list of filenames (not used) + @type list of str + """ + self.updateCatalogs(filenames, withObsolete=True) - def updateSelectedCatalogs(self): - # TODO: implement this with pybabel ... - pass + def updateSelectedCatalogs(self, filenames, withObsolete=False): + """ + Public method to update the selected message catalogs. + + @param filenames list of filenames + @type list of str + @param withObsolete flag indicating to keep obsolete translations + @type bool + """ + title = self.tr("Updating message catalogs") + + locales = {self.__getLocale(f) for f in filenames} + + if len(locales) == 0: + E5MessageBox.warning( + self.__ui, + title, + self.tr('No locales detected. Aborting...')) + return + + if self.__ensurePybabelConfigured(): + workdir = self.getApplication()[0] + translationsDirectory = self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "translationsDirectory")) + potFile = self.__e5project.getAbsoluteUniversalPath( + self.getData("pybabel", "catalogFile")) + argsList = [] + for loc in locales: + args = [ + "update", + "--domain={0}".format(self.getData("pybabel", "domain")), + "--input-file={0}".format( + os.path.relpath(potFile, workdir)), + "--output-dir={0}".format( + os.path.relpath(translationsDirectory, workdir)), + "--locale={0}".format(loc), + ] + if not withObsolete: + args.append("--ignore-obsolete") + argsList.append(args) + + dlg = PyBabelCommandDialog( + self, title=title, + msgSuccess=self.tr("\nMessage catalogs updated successfully.") + ) + res = dlg.startBatchCommand(argsList, workdir) + if res: + dlg.exec() + + def updateSelectedCatalogsObsolete(self, filenames): + """ + Public method to update the message catalogs keeping obsolete + translations. + + @param filenames list of filenames (not used) + @type list of str + """ + self.updateSelectedCatalogs(filenames, withObsolete=True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/PyBabelCommandDialog.py Sat Nov 21 17:50:57 2020 +0100 @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to run a flask command and show its output. +""" + +from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton + +from E5Gui import E5MessageBox + +from .Ui_FlaskCommandDialog import Ui_FlaskCommandDialog + + +class PyBabelCommandDialog(QDialog, Ui_FlaskCommandDialog): + """ + Class implementing a dialog to run a flask command and show its output. + """ + def __init__(self, project, title="", msgSuccess="", msgError="", + parent=None): + """ + Constructor + + @param project reference to the project object + @type Project + @param title window title of the dialog + @type str + @param msgSuccess success message to be shown + @type str + @param msgError message to be shown on error + @type str + @param parent reference to the parent widget + @type QWidget + """ + super(PyBabelCommandDialog, self).__init__(parent) + self.setupUi(self) + + if title: + self.setWindowTitle(title) + + self.__project = project + self.__successMessage = msgSuccess + self.__errorMessage = msgError + + self.__process = None + self.__argsLists = [] + self.__workdir = "" + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + + def startCommand(self, command, args, workdir, clearOutput=True): + """ + Public method to start a pybabel command and show its output. + + @param command pybabel command to be run + @type str + @param args list of command line arguments for the command + @type list of str + @param workdir working directory for the command + @type str + @param clearOutput flag indicating to clear the output + @type bool + @return flag indicating a successful start + @rtype bool + """ + babelCommand = self.__project.getBabelCommand() + + self.__process = QProcess() + self.__process.setWorkingDirectory(workdir) + self.__process.setProcessChannelMode(QProcess.MergedChannels) + + self.__process.readyReadStandardOutput.connect(self.__readStdOut) + self.__process.finished.connect(self.__processFinished) + + if clearOutput: + self.outputEdit.clear() + + babelArgs = [command] + if args: + babelArgs += args + + self.__process.start(babelCommand, babelArgs) + ok = self.__process.waitForStarted(10000) + if not ok: + E5MessageBox.critical( + None, + self.tr("Execute PyBabel Command"), + self.tr("""The pybabel process could not be started.""")) + else: + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setFocus( + Qt.OtherFocusReason) + + return ok + + def startBatchCommand(self, argsLists, workdir): + """ + Public method to start a pybabel command repeatedly with a list of + arguments and show the output. + + @param argsLists list of command line arguments for the batch commands + @type list of lists of str + @param workdir working directory for the command + @type str + @return flag indicating a successful start of the first process + @rtype bool + """ + self.__argsLists = argsLists[:] + self.__workdir = workdir + + # start the first process + args = self.__argsLists.pop(0) + res = self.startCommand(args[0], args[1:], workdir) + if not res: + self.__argsLists = [] + + return res + + def closeEvent(self, evt): + """ + Protected method handling the close event of the dialog. + + @param evt reference to the close event object + @type QCloseEvent + """ + self.__argsLists = [] + self.__cancelProcess() + evt.accept() + + @pyqtSlot() + def __readStdOut(self): + """ + Private slot to add the server process output to the output pane. + """ + if self.__process is not None: + out = str(self.__process.readAllStandardOutput(), "utf-8") + self.outputEdit.insertPlainText(out) + + def __processFinished(self, exitCode, exitStatus): + """ + Private slot connected to the finished signal. + + @param exitCode exit code of the process + @type int + @param exitStatus exit status of the process + @type QProcess.ExitStatus + """ + normal = (exitStatus == QProcess.NormalExit) and (exitCode == 0) + self.__cancelProcess() + + if self.__argsLists: + args = self.__argsLists.pop(0) + self.startCommand(args[0], args[1:], self.__workdir, + clearOutput=False) + return + + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Close).setFocus( + Qt.OtherFocusReason) + + if normal and self.__successMessage: + self.outputEdit.insertPlainText(self.__successMessage) + elif not normal and self.__errorMessage: + self.outputEdit.insertPlainText(self.__errorMessage) + + @pyqtSlot() + def __cancelProcess(self): + """ + Private slot to terminate the current process. + """ + if ( + self.__process is not None and + self.__process.state() != QProcess.NotRunning + ): + self.__process.terminate() + QTimer.singleShot(2000, self.__process.kill) + self.__process.waitForFinished(3000) + + self.__process = None + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot handling presses of the button box buttons. + + @param button reference to the button been clicked + @type QAbstractButton + """ + if button is self.buttonBox.button(QDialogButtonBox.Close): + self.close() + elif button is self.buttonBox.button(QDialogButtonBox.Cancel): + self.__argsLists = [] + self.__cancelProcess()
--- a/ProjectFlask/PyBabelConfigDialog.py Thu Nov 19 20:19:55 2020 +0100 +++ b/ProjectFlask/PyBabelConfigDialog.py Sat Nov 21 17:50:57 2020 +0100 @@ -7,6 +7,8 @@ Module implementing a dialog to edit the PyBabel configuration. """ +import os + from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtWidgets import QDialog, QDialogButtonBox @@ -43,6 +45,11 @@ self.configFilePicker.setDefaultDirectory( self.__e5project.getProjectPath()) + self.translationsDirectoryPicker.setMode( + E5PathPickerModes.DirectoryMode) + self.translationsDirectoryPicker.setDefaultDirectory( + self.__e5project.getProjectPath()) + self.catalogFilePicker.setMode( E5PathPickerModes.SaveFileEnsureExtensionMode) self.catalogFilePicker.setFilters(self.tr( @@ -60,6 +67,12 @@ self.configFilePicker.setText( self.__e5project.getAbsoluteUniversalPath( configuration["configFile"])) + if "translationsDirectory" in configuration: + self.translationsDirectoryPicker.setText( + self.__e5project.getAbsoluteUniversalPath( + configuration["translationsDirectory"])) + if "domain" in configuration: + self.domainEdit.setText(configuration["domain"]) if "catalogFile" in configuration: self.catalogFilePicker.setText( self.__e5project.getAbsoluteUniversalPath( @@ -80,21 +93,50 @@ configuration = { "configFile": self.__e5project.getRelativeUniversalPath( self.configFilePicker.text()), - "catalogFile": self.__e5project.getRelativeUniversalPath( - self.catalogFilePicker.text()), + "translationsDirectory": self.__e5project.getRelativeUniversalPath( + self.translationsDirectoryPicker.text()), } + + domain = self.domainEdit.text() + if domain: + configuration["domain"] = domain + else: + configuration["domain"] = "messages" + + catalogFile = self.catalogFilePicker.text() + if not catalogFile: + # use a default name made of translations dir and domain + catalogFile = os.path.join( + configuration["translationsDirectory"], + "{0}.pot".format(configuration["domain"])) + configuration["catalogFile"] = ( + self.__e5project.getRelativeUniversalPath(catalogFile) + ) + if self.markersEdit.text(): configuration["markersList"] = self.markersEdit.text().split() return configuration def __updateOK(self): + """ + Private method to update the status of the OK button. + """ enable = ( bool(self.configFilePicker.text()) and - bool(self.catalogFilePicker.text()) + bool(self.translationsDirectoryPicker.text()) ) self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) + def __updateCatalogPicker(self): + """ + Private method to update the contents of the catalog picker. + """ + translationsDirectory = self.translationsDirectoryPicker.text() + domain = self.domainEdit.text() + self.catalogFilePicker.setText(os.path.join( + translationsDirectory, "{0}.pot".format(domain))) + @pyqtSlot(str) def on_configFilePicker_textChanged(self, txt): """ @@ -106,7 +148,7 @@ self.__updateOK() @pyqtSlot(str) - def on_catalogFilePicker_textChanged(self, txt): + def on_translationsDirectoryPicker_textChanged(self, txt): """ Private slot to handle a change of the catalog file name. @@ -114,3 +156,14 @@ @type str """ self.__updateOK() + self.__updateCatalogPicker() + + @pyqtSlot(str) + def on_domainEdit_textChanged(self, txt): + """ + Private slot to handle a change of the translations domain. + + @param txt entered translations domain + @type str + """ + self.__updateCatalogPicker()
--- a/ProjectFlask/PyBabelConfigDialog.ui Thu Nov 19 20:19:55 2020 +0100 +++ b/ProjectFlask/PyBabelConfigDialog.ui Sat Nov 21 17:50:57 2020 +0100 @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>600</width> - <height>124</height> + <height>150</height> </rect> </property> <property name="windowTitle"> @@ -24,40 +24,6 @@ </property> </widget> </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Message Catalog:</string> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>Translation Markers:</string> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QLineEdit" name="markersEdit"> - <property name="toolTip"> - <string>Enter the translation markers separated by space (_ is included by default)</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="3" column="0" colspan="2"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> <item row="0" column="1"> <widget class="E5PathPicker" name="configFilePicker" native="true"> <property name="sizePolicy"> @@ -74,7 +40,54 @@ </property> </widget> </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Translations Directory:</string> + </property> + </widget> + </item> <item row="1" column="1"> + <widget class="E5PathPicker" name="translationsDirectoryPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the name of the directory containing the translations</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Domain:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="domainEdit"> + <property name="toolTip"> + <string>Enter the name of the translations domain (leave empty for default)</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Message Catalog:</string> + </property> + </widget> + </item> + <item row="3" column="1"> <widget class="E5PathPicker" name="catalogFilePicker" native="true"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> @@ -90,6 +103,33 @@ </property> </widget> </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Translation Markers:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="markersEdit"> + <property name="toolTip"> + <string>Enter the translation markers separated by space (_ is included by default)</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> </layout> </widget> <customwidgets> @@ -102,6 +142,8 @@ </customwidgets> <tabstops> <tabstop>configFilePicker</tabstop> + <tabstop>translationsDirectoryPicker</tabstop> + <tabstop>domainEdit</tabstop> <tabstop>catalogFilePicker</tabstop> <tabstop>markersEdit</tabstop> </tabstops>