Continued implementing various flask actions.

Sat, 14 Nov 2020 19:56:06 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 14 Nov 2020 19:56:06 +0100
changeset 9
79094fb72c18
parent 8
cfbd3a2757fd
child 10
506c78268b18

Continued implementing various flask actions.

PluginFlask.e4p file | annotate | diff | comparison | revisions
PluginProjectFlask.py file | annotate | diff | comparison | revisions
ProjectFlask/ConfigurationPage/FlaskPage.py file | annotate | diff | comparison | revisions
ProjectFlask/ConfigurationPage/FlaskPage.ui file | annotate | diff | comparison | revisions
ProjectFlask/FlaskCommandDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskCommandDialog.ui file | annotate | diff | comparison | revisions
ProjectFlask/Project.py file | annotate | diff | comparison | revisions
ProjectFlask/RunServerDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/RunServerDialog.ui file | annotate | diff | comparison | revisions
ProjectFlask/ServerStartOptionsDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/ServerStartOptionsDialog.ui file | annotate | diff | comparison | revisions
--- a/PluginFlask.e4p	Fri Nov 13 19:51:28 2020 +0100
+++ b/PluginFlask.e4p	Sat Nov 14 19:56:06 2020 +0100
@@ -18,16 +18,20 @@
     <Source>ProjectFlask/AnsiTools.py</Source>
     <Source>ProjectFlask/ConfigurationPage/FlaskPage.py</Source>
     <Source>ProjectFlask/ConfigurationPage/__init__.py</Source>
+    <Source>ProjectFlask/FlaskCommandDialog.py</Source>
     <Source>ProjectFlask/Project.py</Source>
     <Source>ProjectFlask/RoutesDialog.py</Source>
     <Source>ProjectFlask/RunServerDialog.py</Source>
+    <Source>ProjectFlask/ServerStartOptionsDialog.py</Source>
     <Source>ProjectFlask/__init__.py</Source>
     <Source>__init__.py</Source>
   </Sources>
   <Forms>
     <Form>ProjectFlask/ConfigurationPage/FlaskPage.ui</Form>
+    <Form>ProjectFlask/FlaskCommandDialog.ui</Form>
     <Form>ProjectFlask/RoutesDialog.ui</Form>
     <Form>ProjectFlask/RunServerDialog.ui</Form>
+    <Form>ProjectFlask/ServerStartOptionsDialog.ui</Form>
   </Forms>
   <Others>
     <Other>.hgignore</Other>
--- a/PluginProjectFlask.py	Fri Nov 13 19:51:28 2020 +0100
+++ b/PluginProjectFlask.py	Sat Nov 14 19:56:06 2020 +0100
@@ -134,41 +134,18 @@
             "VirtualEnvironmentNamePy3": "",
             
             "FlaskDocUrl": "https://flask.palletsprojects.com",
-#            "Python3ConsoleType": "ipython",
-#            
-#            "ServerAddress": "",
-#            
-#            "RecentNumberApps": 10,
-#            "UseIPv6": False,
-#            "UseThreading": True,
-#            
-#            "TranslationsEditor": "",
-#            "FuzzyTranslations": False,
             
             "UseExternalBrowser": False,
-#            
-#            "CheckDeployMode": False,
-#            
-#            "RecentNumberTestData": 10,
-#            "KeepTestDatabase": False,
-#            
-#            "RecentNumberDatabaseNames": 10,
         }
         if isWindowsPlatform():
             self.__defaults["AnsiColorScheme"] = "Windows 10"
+            self.__defaults["ConsoleCommand"] = "cmd.exe /c"
         elif isMacPlatform():
             self.__defaults["AnsiColorScheme"] = "xterm"
+            self.__defaults["ConsoleCommand"] = "xterm -e"
         else:
             self.__defaults["AnsiColorScheme"] = "Ubuntu"
-#        if isWindowsPlatform():
-#            self.__defaults["ConsoleCommandNoClose"] = "cmd.exe /k"
-#            self.__defaults["ConsoleCommand"] = "cmd.exe /c"
-#        elif isMacPlatform():
-#            self.__defaults["ConsoleCommandNoClose"] = "xterm -hold -e"
-#            self.__defaults["ConsoleCommand"] = "xterm -e"
-#        else:
-#            self.__defaults["ConsoleCommandNoClose"] = "konsole --noclose -e"
-#            self.__defaults["ConsoleCommand"] = "konsole -e"
+            self.__defaults["ConsoleCommand"] = "konsole -e"
         
         self.__translator = None
         self.__loadTranslator()
--- a/ProjectFlask/ConfigurationPage/FlaskPage.py	Fri Nov 13 19:51:28 2020 +0100
+++ b/ProjectFlask/ConfigurationPage/FlaskPage.py	Sat Nov 14 19:56:06 2020 +0100
@@ -18,10 +18,11 @@
 
 import UI.PixmapCache
 
+from Globals import isWindowsPlatform, isMacPlatform
+
 from .. import AnsiTools
 
 
-# TODO: add selection for the ANSI color scheme (see MicroPython)
 class FlaskPage(ConfigurationPageBase, Ui_FlaskPage):
     """
     Class implementing the Flask configuration page.
@@ -36,11 +37,25 @@
         self.setupUi(self)
         self.setObjectName("FlaskPage")
         
+        self.__plugin = plugin
+        
+        consoleList = []
+        if isWindowsPlatform():
+            consoleList.append("cmd.exe /c")
+        elif isMacPlatform():
+            consoleList.append("xterm -e")
+            consoleList.append("/opt/X11/bin/xterm -e")
+        else:
+            consoleList.append("konsole -e")
+            consoleList.append("gnome-terminal -e")
+            consoleList.append("mate-terminal -e")
+            consoleList.append("xfce4-terminal -e")
+            consoleList.append("xterm -e")
+        self.consoleCommandCombo.addItems(consoleList)
+        
         self.colorSchemeComboBox.addItems(
             sorted(AnsiTools.getAvailableColorSchemes()))
         
-        self.__plugin = plugin
-        
         self.urlResetButton.setIcon(
             UI.PixmapCache.getIcon("editUndo"))
         self.py3VenvNamesReloadButton.setIcon(
@@ -51,6 +66,9 @@
             [""] + sorted(venvManager.getVirtualenvNames()))
         
         # set initial values
+        self.consoleCommandCombo.setEditText(
+            self.__plugin.getPreferences("ConsoleCommand"))
+        
         self.externalBrowserCheckBox.setChecked(
             self.__plugin.getPreferences("UseExternalBrowser"))
         
@@ -74,6 +92,9 @@
         Public slot to save the Flask configuration.
         """
         self.__plugin.setPreferences(
+            "ConsoleCommand", self.consoleCommandCombo.currentText())
+        
+        self.__plugin.setPreferences(
             "UseExternalBrowser", self.externalBrowserCheckBox.isChecked())
         
         self.__plugin.setPreferences(
--- a/ProjectFlask/ConfigurationPage/FlaskPage.ui	Fri Nov 13 19:51:28 2020 +0100
+++ b/ProjectFlask/ConfigurationPage/FlaskPage.ui	Sat Nov 14 19:56:06 2020 +0100
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>500</width>
-    <height>373</height>
+    <height>508</height>
    </rect>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
@@ -32,6 +32,38 @@
     </widget>
    </item>
    <item>
+    <widget class="QGroupBox" name="pyramidConsoleGroup">
+     <property name="title">
+      <string>Console Command</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_3">
+      <item>
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>Console Command:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="consoleCommandCombo">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="toolTip">
+         <string>Enter the console command</string>
+        </property>
+        <property name="editable">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
     <widget class="QGroupBox" name="flaskBrowserGroup">
      <property name="title">
       <string>Web-Browser</string>
@@ -158,6 +190,7 @@
   </layout>
  </widget>
  <tabstops>
+  <tabstop>consoleCommandCombo</tabstop>
   <tabstop>externalBrowserCheckBox</tabstop>
   <tabstop>py3VenvNameComboBox</tabstop>
   <tabstop>py3VenvNamesReloadButton</tabstop>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskCommandDialog.py	Sat Nov 14 19:56:06 2020 +0100
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+
+"""
+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 FlaskCommandDialog(QDialog, Ui_FlaskCommandDialog):
+    """
+    Class implementing a dialog to run a flask command and show its output.
+    """
+    def __init__(self, project, parent=None):
+        """
+        Constructor
+        
+        @param project reference to the project object
+        @type Project
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super(FlaskCommandDialog, self).__init__(parent)
+        self.setupUi(self)
+        
+        self.__project = project
+        
+        self.__process = None
+        
+        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=None):
+        """
+        Public method to start a flask command and show its output.
+        
+        @param command flask command to be run
+        @type str
+        @param args list of command line arguments for the command
+        @type list of str
+        @return flag indicating a successful start
+        @rtype bool
+        """
+        workdir, env = self.__project.prepareRuntimeEnvironment()
+        if env is not None:
+            flaskCommand = self.__project.getFlaskCommand()
+            
+            self.__process = QProcess()
+            self.__process.setProcessEnvironment(env)
+            self.__process.setWorkingDirectory(workdir)
+            self.__process.setProcessChannelMode(QProcess.MergedChannels)
+            
+            self.__process.readyReadStandardOutput.connect(self.__readStdOut)
+            self.__process.finished.connect(self.__processFinished)
+            
+            self.outputEdit.clear()
+            
+            flaskArgs = [command]
+            if args:
+                flaskArgs += args
+            
+            self.__process.start(flaskCommand, flaskArgs)
+            ok = self.__process.waitForStarted(10000)
+            if not ok:
+                E5MessageBox.critical(
+                    None,
+                    self.tr("Execute Flask Command"),
+                    self.tr("""The Flask 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)
+        else:
+            ok = False
+        
+        return ok
+    
+    def closeEvent(self, evt):
+        """
+        Protected method handling the close event of the dialog.
+        
+        @param evt reference to the close event object
+        @type QCloseEvent
+        """
+        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)
+    
+    @pyqtSlot()
+    def __processFinished(self):
+        """
+        Private slot handling the finishing of the server process.
+        """
+        self.__cancelProcess()
+        
+        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)
+    
+    @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):
+        """
+        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.__cancelProcess()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/FlaskCommandDialog.ui	Sat Nov 14 19:56:06 2020 +0100
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FlaskCommandDialog</class>
+ <widget class="QDialog" name="FlaskCommandDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>500</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Flask Command</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Output</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QPlainTextEdit" name="outputEdit">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="lineWrapMode">
+         <enum>QPlainTextEdit::NoWrap</enum>
+        </property>
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>outputEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>FlaskCommandDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>505</x>
+     <y>474</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>593</x>
+     <y>419</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- a/ProjectFlask/Project.py	Fri Nov 13 19:51:28 2020 +0100
+++ b/ProjectFlask/Project.py	Sat Nov 14 19:56:06 2020 +0100
@@ -10,7 +10,7 @@
 import os
 
 from PyQt5.QtCore import (
-    pyqtSlot, QObject, QProcess, QProcessEnvironment
+    pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer
 )
 from PyQt5.QtWidgets import QMenu
 
@@ -25,6 +25,7 @@
 
 from .RunServerDialog import RunServerDialog
 from .RoutesDialog import RoutesDialog
+from .FlaskCommandDialog import FlaskCommandDialog
 
 
 class Project(QObject):
@@ -53,10 +54,10 @@
         
         self.__menus = {}   # dictionary with references to menus
          
-##        self.__serverProc = None
         self.__serverDialog = None
         self.__routesDialog = None
-       
+        self.__shellProcess = None
+        
         self.__flaskVersions = {
             "python": "",
             "flask": "",
@@ -101,9 +102,43 @@
         self.runDevServerAct.triggered.connect(self.__runDevelopmentServer)
         self.actions.append(self.runDevServerAct)
         
-        ##############################
+        self.askForServerOptionsAct = E5Action(
+            self.tr('Ask for Server Start Options'),
+            self.tr('Ask for Server Start Options'),
+            0, 0,
+            self, 'flask_ask_server_options')
+        self.askForServerOptionsAct.setStatusTip(self.tr(
+            'Ask for server start options'))
+        self.askForServerOptionsAct.setWhatsThis(self.tr(
+            """<b>Ask for Server Start Options</b>"""
+            """<p>Asks for server start options before the Flask Web server"""
+            """ is started. If this is unchecked, the server is started with"""
+            """ default parameters.</p>"""
+        ))
+        self.askForServerOptionsAct.setCheckable(True)
+        self.actions.append(self.askForServerOptionsAct)
+        
+        ###############################
+        ## shell action below        ##
+        ###############################
+        
+        self.runPythonShellAct = E5Action(
+            self.tr('Start Flask Python Console'),
+            self.tr('Start Flask &Python Console'),
+            0, 0,
+            self, 'flask_python_console')
+        self.runPythonShellAct.setStatusTip(self.tr(
+            'Starts an interactive Python interpreter'))
+        self.runPythonShellAct.setWhatsThis(self.tr(
+            """<b>Start Flask Python Console</b>"""
+            """<p>Starts an interactive Python interpreter.</p>"""
+        ))
+        self.runPythonShellAct.triggered.connect(self.__runPythonShell)
+        self.actions.append(self.runPythonShellAct)
+        
+        ################################
         ## routes action below        ##
-        ##############################
+        ################################
         
         self.showRoutesAct = E5Action(
             self.tr('Show Routes'),
@@ -120,6 +155,25 @@
         self.actions.append(self.showRoutesAct)
         
         ##################################
+        ## database action below        ##
+        ##################################
+        
+        self.initDatabaseAct = E5Action(
+            self.tr('Initialize Database'),
+            self.tr('&Initialize Database'),
+            0, 0,
+            self, 'flask_init_database')
+        self.initDatabaseAct.setStatusTip(self.tr(
+            'Shows a dialog with the result of the database creation'))
+        self.initDatabaseAct.setWhatsThis(self.tr(
+            """<b>Initialize Database</b>"""
+            """<p>Shows a dialog with the result of the database"""
+            """ creation.</p>"""
+        ))
+        self.initDatabaseAct.triggered.connect(self.__initDatabase)
+        self.actions.append(self.initDatabaseAct)
+        
+        ##################################
         ## documentation action below   ##
         ##################################
         
@@ -167,11 +221,17 @@
         menu = QMenu(self.tr('&Flask'), self.__ui)
         menu.setTearOffEnabled(True)
         
+        menu.addSection("flask run")
         menu.addAction(self.runServerAct)
         menu.addAction(self.runDevServerAct)
-        menu.addSeparator()
+        menu.addAction(self.askForServerOptionsAct)
+        menu.addSection("flask shell")
+        menu.addAction(self.runPythonShellAct)
+        menu.addSection("flask routes")
         menu.addAction(self.showRoutesAct)
-        menu.addSeparator()
+        menu.addSection("flask init-db")
+        menu.addAction(self.initDatabaseAct)
+        menu.addSection(self.tr("Various"))
         menu.addAction(self.documentationAct)
         menu.addSeparator()
         menu.addAction(self.aboutFlaskAct)
@@ -421,7 +481,7 @@
         self.__ui.launchHelpViewer(page)
     
     ##################################################################
-    ## slots below implement run functions
+    ## slots below implement run functions for the server
     ##################################################################
     
     @pyqtSlot()
@@ -435,8 +495,10 @@
         if self.__serverDialog is not None:
             self.__serverDialog.close()
         
-        dlg = RunServerDialog(self.__plugin)
-        if dlg.startServer(self, development=development):
+        askForOptions = self.askForServerOptionsAct.isChecked()
+        dlg = RunServerDialog(self.__plugin, self)
+        if dlg.startServer(development=development,
+                           askForOptions=askForOptions):
             dlg.show()
             self.__serverDialog = dlg
     
@@ -447,31 +509,60 @@
         """
         self.__runServer(development=True)
     
-    # TODO: add method to start a server with parameters
+    ##################################################################
+    ## slots below implement functions for the flask console
+    ##################################################################
     
-##    def __serverProcFinished(self):
-##        """
-##        Private slot connected to the finished signal.
-##        """
-##        if (
-##            self.__serverProc is not None and
-##            self.__serverProc.state() != QProcess.NotRunning
-##        ):
-##            self.__serverProc.terminate()
-##            QTimer.singleShot(2000, self.__serverProc.kill)
-##            self.__serverProc.waitForFinished(3000)
-##        self.__serverProc = None
-    
+    @pyqtSlot()
     def __runPythonShell(self):
         """
         Private slot to start a Python console in the app context.
         """
-        # TODO: implement this (flask shell)
+        workdir, env = self.prepareRuntimeEnvironment()
+        if env is not None:
+            command = self.getFlaskCommand()
+            
+            consoleCmd = self.__plugin.getPreferences("ConsoleCommand")
+            if consoleCmd:
+                self.__terminatePythonShell()
+                
+                args = Utilities.parseOptionString(consoleCmd)
+                args[0] = Utilities.getExecutablePath(args[0])
+                args += [command, "shell"]
+                
+                self.__shellProcess = QProcess()
+                self.__shellProcess.setProcessEnvironment(env)
+                self.__shellProcess.setWorkingDirectory(workdir)
+                self.__shellProcess.finished.connect(
+                    self.__shellProcessFinished)
+                
+                self.__shellProcess.start(args[0], args[1:])
+                self.__shellProcess.waitForStarted(10000)
+    
+    @pyqtSlot()
+    def __shellProcessFinished(self):
+        """
+        Private slot connected to the finished signal.
+        """
+        self.__shellProcess = None
+    
+    def __terminatePythonShell(self):
+        """
+        Private method to terminate the current Python console.
+        """
+        if (
+            self.__shellProcess is not None and
+            self.__shellProcess.state() != QProcess.NotRunning
+        ):
+            self.__shellProcess.terminate()
+            QTimer.singleShot(2000, self.__shellProcess.kill)
+            self.__shellProcess.waitForFinished(3000)
     
     ##################################################################
     ## slots below implement various debugging functions
     ##################################################################
     
+    @pyqtSlot()
     def __showRoutes(self):
         """
         Private slot showing all URL dispatch routes.
@@ -483,3 +574,12 @@
         if dlg.showRoutes():
             dlg.show()
             self.__routesDialog = dlg
+    
+    @pyqtSlot()
+    def __initDatabase(self):
+        """
+        Private slot showing the result of the database creation.
+        """
+        dlg = FlaskCommandDialog(self)
+        if dlg.startCommand("init-db"):
+            dlg.exec()
--- a/ProjectFlask/RunServerDialog.py	Fri Nov 13 19:51:28 2020 +0100
+++ b/ProjectFlask/RunServerDialog.py	Sat Nov 14 19:56:06 2020 +0100
@@ -11,7 +11,7 @@
 
 from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer
 from PyQt5.QtGui import QTextCharFormat
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMenu
 
 from E5Gui import E5MessageBox
 from E5Gui.E5Application import e5App
@@ -19,6 +19,9 @@
 from .Ui_RunServerDialog import Ui_RunServerDialog
 
 from . import AnsiTools
+from .ServerStartOptionsDialog import ServerStartOptionsDialog
+
+import UI.PixmapCache
 
 
 # TODO: should this be placed into the sidebar as a sidebar widget?
@@ -26,12 +29,14 @@
     """
     Class implementing a dialog to run the Flask server.
     """
-    def __init__(self, plugin, parent=None):
+    def __init__(self, plugin, project, parent=None):
         """
         Constructor
         
         @param plugin reference to the plug-in object
         @type PluginProjectFlask
+        @param project reference to the project object
+        @type Project
         @param parent reference to the parent widget
         @type QWidget
         """
@@ -39,11 +44,16 @@
         self.setupUi(self)
         
         self.__plugin = plugin
+        self.__project = project
+        
+        self.__serverOptions = {
+            "development": False
+        }
         
         self.__process = None
         self.__serverUrl = ""
         
-        self.__ansiRe = re.compile(r"""(\\x1b\[\d+m)""")
+        self.__ansiRe = re.compile("(\\x1b\[\d+m)")
         
         self.__urlRe = re.compile(r""" * Running on ([^(]+) \(.*""")
         
@@ -51,22 +61,67 @@
         self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
         
         self.__defaultTextFormat = self.outputEdit.currentCharFormat()
+        
+        self.__initActionsMenu()
     
-    def startServer(self, project, development=False):
+    def __initActionsMenu(self):
+        """
+        Private method to populate the actions button menu.
+        """
+        self.__actionsMenu = QMenu()
+        self.__actionsMenu.setTearOffEnabled(True)
+        self.__actionsMenu.setToolTipsVisible(True)
+        self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu)
+        
+        # re-start server
+        self.__actionsMenu.addAction(
+            self.tr("Re-start Server"), self.__restartServer)
+        self.__restartModeAct = self.__actionsMenu.addAction(
+            self.tr("Re-start Server"), self.__restartServerDifferentMode)
+        self.__actionsMenu.addSeparator()
+        self.__actionsMenu.addAction(
+            self.tr("Re-start Server with Options"),
+            self.__restartServerWithOptions)
+        # start server with options
+        
+        self.menuButton.setIcon(UI.PixmapCache.getIcon("actionsToolButton"))
+        self.menuButton.setMenu(self.__actionsMenu)
+    
+    @pyqtSlot()
+    def __showActionsMenu(self):
+        """
+        Private slot handling the actions menu about to be shown.
+        """
+        if self.__serverOptions["development"]:
+            self.__restartModeAct.setText(
+                self.tr("Re-start Server (Production Mode)"))
+        else:
+            self.__restartModeAct.setText(
+                self.tr("Re-start Server (Development Mode)"))
+    
+    def startServer(self, development=False, restart=False,
+                    askForOptions=False):
         """
         Public method to start the Flask server process.
         
-        @param project reference to the project object
-        @type Project
         @param development flag indicating development mode
         @type bool
         @return flag indicating success
         @rtype bool
         """
-        workdir, env = project.prepareRuntimeEnvironment(
-            development=development)
+        self.__serverOptions["development"] = development
+        
+        if askForOptions:
+            dlg = ServerStartOptionsDialog(self.__serverOptions)
+            if dlg.exec() != QDialog.Accepted:
+                return False
+            
+            self.__serverOptions.update(dlg.getDataDict())
+        
+        workdir, env = self.__project.prepareRuntimeEnvironment(
+            development=self.__serverOptions["development"])
         if env is not None:
-            command = project.getFlaskCommand()
+            command = self.__project.getFlaskCommand()
             
             self.__process = QProcess()
             self.__process.setProcessEnvironment(env)
@@ -76,11 +131,13 @@
             self.__process.readyReadStandardOutput.connect(self.__readStdOut)
             self.__process.finished.connect(self.__processFinished)
             
+            self.outputEdit.clear()
+            
             args = ["run"]
-#            if host:
-#                args += ["--host", host]
-#            if port:
-#                args += ["--port", str(port)]
+            if "host" in self.__serverOptions and self.__serverOptions["host"]:
+                args += ["--host", self.__serverOptions["host"]]
+            if "port" in self.__serverOptions and self.__serverOptions["port"]:
+                args += ["--port", self.__serverOptions["port"]]
             
             self.__process.start(command, args)
             ok = self.__process.waitForStarted(10000)
@@ -94,6 +151,7 @@
                 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
                 self.stopServerButton.setEnabled(True)
                 self.stopServerButton.setDefault(True)
+                self.startBrowserButton.setEnabled(True)
         else:
             ok = False
         
@@ -120,7 +178,6 @@
                 urlMatch = self.__urlRe.search(out)
                 if urlMatch:
                     self.__serverUrl = urlMatch.group(1)
-                    self.startBrowserButton.setEnabled(True)
             
             for txt in self.__ansiRe.split(out):
                 if txt.startswith("\x1b["):
@@ -180,3 +237,36 @@
         else:
             e5App().getObject("UserInterface").launchHelpViewer(
                 self.__serverUrl)
+    
+    @pyqtSlot()
+    def __restartServer(self):
+        """
+        Private slot to restart the server process.
+        """
+        # step 1: stop the current server
+        self.on_stopServerButton_clicked()
+        
+        # step 2: start a new server
+        self.startServer(development=self.__serverOptions["development"])
+    
+    @pyqtSlot()
+    def __restartServerDifferentMode(self):
+        """
+        Private slot to restart the server process with the opposite mode.
+        """
+        # step 1: stop the current server
+        self.on_stopServerButton_clicked()
+        
+        # step 2: start a new server
+        self.startServer(development=not self.__serverOptions["development"])
+    @pyqtSlot()
+    def __restartServerWithOptions(self):
+        """
+        Private slot to restart the server asking for start options.
+        """
+        # step 1: stop the current server
+        self.on_stopServerButton_clicked()
+        
+        # step 2: start a new server
+        self.startServer(development=self.__serverOptions["development"],
+                         askForOptions=True)
--- a/ProjectFlask/RunServerDialog.ui	Fri Nov 13 19:51:28 2020 +0100
+++ b/ProjectFlask/RunServerDialog.ui	Sat Nov 14 19:56:06 2020 +0100
@@ -45,6 +45,13 @@
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
+      <widget class="QToolButton" name="menuButton">
+       <property name="popupMode">
+        <enum>QToolButton::InstantPopup</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
       <widget class="QPushButton" name="startBrowserButton">
        <property name="enabled">
         <bool>false</bool>
@@ -99,6 +106,7 @@
  </widget>
  <tabstops>
   <tabstop>outputEdit</tabstop>
+  <tabstop>menuButton</tabstop>
   <tabstop>startBrowserButton</tabstop>
   <tabstop>stopServerButton</tabstop>
  </tabstops>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/ServerStartOptionsDialog.py	Sat Nov 14 19:56:06 2020 +0100
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to enter parameters to start the server.
+"""
+
+from PyQt5.QtWidgets import QDialog
+
+from .Ui_ServerStartOptionsDialog import Ui_ServerStartOptionsDialog
+
+
+class ServerStartOptionsDialog(QDialog, Ui_ServerStartOptionsDialog):
+    """
+    Class implementing a dialog to enter parameters to start the server.
+    """
+    def __init__(self, options, parent=None):
+        """
+        Constructor
+        
+        @param options dictionary containing the current server start options
+        @type dict
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super(ServerStartOptionsDialog, self).__init__(parent)
+        self.setupUi(self)
+        
+        self.developmentCheckBox.setChecked(options.get("development", False))
+        self.hostEdit.setText(options.get("host", ""))
+        self.portSpinBox.setValue(int(options.get("port", "5000")))
+        
+        msh = self.minimumSizeHint()
+        self.resize(max(self.width(), msh.width()), msh.height())
+    
+    def getDataDict(self):
+        """
+        Public method to get a dictionary containing the entered data.
+        
+        @return dictionary containing the entered data
+        @rtype dict
+        """
+        options = {}
+        
+        options["development"] = self.developmentCheckBox.isChecked()
+        host = self.hostEdit.text()
+        if host:
+            options["host"] = host
+        port = self.portSpinBox.value()
+        if port != 5000:
+            options["port"] = str(port)
+        
+        return options
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ProjectFlask/ServerStartOptionsDialog.ui	Sat Nov 14 19:56:06 2020 +0100
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ServerStartOptionsDialog</class>
+ <widget class="QDialog" name="ServerStartOptionsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>141</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Server Parameters</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QGridLayout" name="gridLayout">
+     <item row="0" column="0" colspan="3">
+      <widget class="QCheckBox" name="developmentCheckBox">
+       <property name="toolTip">
+        <string>Select to start the server in development mode</string>
+       </property>
+       <property name="text">
+        <string>Development Mode</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Host:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1" colspan="2">
+      <widget class="QLineEdit" name="hostEdit">
+       <property name="toolTip">
+        <string>Enter the interface to bind to</string>
+       </property>
+       <property name="clearButtonEnabled">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Port:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
+      <widget class="QSpinBox" name="portSpinBox">
+       <property name="toolTip">
+        <string>Enter the port to bind to</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+       <property name="minimum">
+        <number>1025</number>
+       </property>
+       <property name="maximum">
+        <number>65565</number>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="2">
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>188</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <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>
+ <tabstops>
+  <tabstop>developmentCheckBox</tabstop>
+  <tabstop>hostEdit</tabstop>
+  <tabstop>portSpinBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>ServerStartOptionsDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>ServerStartOptionsDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

eric ide

mercurial