Implemented two foundational dialogs for executing pipx commands and running applications from the list.

Mon, 24 Jun 2024 19:48:46 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 24 Jun 2024 19:48:46 +0200
changeset 4
097a06104774
parent 3
e73fb9827b74
child 5
e7c41d19f66f

Implemented two foundational dialogs for executing pipx commands and running applications from the list.

PipxInterface/PipxAppStartDialog.py file | annotate | diff | comparison | revisions
PipxInterface/PipxAppStartDialog.ui file | annotate | diff | comparison | revisions
PipxInterface/PipxDialog.py file | annotate | diff | comparison | revisions
PipxInterface/PipxDialog.ui file | annotate | diff | comparison | revisions
PipxInterface/PipxWidget.py file | annotate | diff | comparison | revisions
PluginPipxInterface.epj file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxAppStartDialog.py	Mon Jun 24 19:48:46 2024 +0200
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+
+"""
+Module implementing a dialog to enter the application command line parameters and
+to execute the app.
+"""
+
+import os
+import shlex
+
+from PyQt6.QtCore import QCoreApplication, QProcess, Qt, QTimer, pyqtSlot
+from PyQt6.QtWidgets import QAbstractButton, QDialog, QDialogButtonBox
+
+from eric7 import Preferences
+from eric7.EricGui import EricPixmapCache
+from eric7.EricWidgets import EricMessageBox
+
+from .Ui_PipxAppStartDialog import Ui_PipxAppStartDialog
+
+
+class PipxAppStartDialog(QDialog, Ui_PipxAppStartDialog):
+    """
+    Class implementing a dialog to enter the application command line parameters and
+    to execute the app.
+    """
+
+    def __init__(self, app, parent=None):
+        """
+        Constructor
+
+        @param app path of the application to be executed
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.executeButton.setIcon(EricPixmapCache.getIcon("start"))
+
+        self.__process = None
+
+        self.appLabel.setText(app)
+        self.errorGroup.hide()
+
+        self.parametersEdit.returnPressed.connect(self.on_executeButton_clicked)
+
+    def closeEvent(self, e):
+        """
+        Protected slot implementing a close event handler.
+
+        @param e close event
+        @type QCloseEvent
+        """
+        self.__cancel()
+        e.accept()
+
+    def __finish(self):
+        """
+        Private slot called when the process finished or the user pressed
+        the button.
+        """
+        if (
+            self.__process is not None
+            and self.__process.state() != QProcess.ProcessState.NotRunning
+        ):
+            self.__process.terminate()
+            QTimer.singleShot(2000, self.__process.kill)
+            self.__process.waitForFinished(3000)
+
+        self.__process = None
+
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
+        self.executeButton.setEnabled(True)
+
+        self.parametersEdit.setFocus(Qt.FocusReason.OtherFocusReason)
+        self.parametersEdit.selectAll()
+
+    def __cancel(self):
+        """
+        Private slot to cancel the current action.
+        """
+        self.__finish()
+
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot called by a button of the button box clicked.
+
+        @param button button that was clicked
+        @type QAbstractButton
+        """
+        if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close):
+            self.close()
+        elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel):
+            self.__cancel()
+
+    @pyqtSlot(int, QProcess.ExitStatus)
+    def __procFinished(self, _exitCode, _exitStatus):
+        """
+        Private slot connected to the finished signal.
+
+        @param _exitCode exit code of the process (unused)
+        @type int
+        @param _exitStatus exit status of the process (unused)
+        @type QProcess.ExitStatus
+        """
+        self.__finish()
+
+    @pyqtSlot()
+    def on_executeButton_clicked(self):
+        """
+        Private slot to execute the selected app with the entered parameters.
+        """
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
+        self.executeButton.setEnabled(False)
+
+        command = self.parametersEdit.text()
+        args = shlex.split(command)
+
+        self.__process = QProcess()
+        self.__process.finished.connect(self.__procFinished)
+        self.__process.readyReadStandardOutput.connect(self.__readStdout)
+        self.__process.readyReadStandardError.connect(self.__readStderr)
+        self.__process.start(self.appLabel.text(), args)
+        procStarted = self.__process.waitForStarted(5000)
+        if not procStarted:
+            self.buttonBox.setFocus()
+            EricMessageBox.critical(
+                self,
+                self.tr("Process Generation Error"),
+                self.tr("The process {0} could not be started.").format(
+                    os.path.basename(self.appLabel.text())
+                ),
+            )
+
+    def __readStdout(self):
+        """
+        Private slot to handle the readyReadStandardOutput signal.
+
+        It reads the output of the process, formats it and inserts it into
+        the contents pane.
+        """
+        if self.__process is not None:
+            txt = str(
+                self.__process.readAllStandardOutput(),
+                Preferences.getSystem("IOEncoding"),
+                "replace",
+            )
+            self.__addOutput(txt)
+
+    def __addOutput(self, txt):
+        """
+        Private method to add some text to the output pane.
+
+        @param txt text to be added
+        @type str
+        """
+        self.resultbox.insertPlainText(txt)
+        self.resultbox.ensureCursorVisible()
+        QCoreApplication.processEvents()
+
+    def __readStderr(self):
+        """
+        Private slot to handle the readyReadStandardError signal.
+
+        It reads the error output of the process and inserts it into the
+        error pane.
+        """
+        if self.__process is not None:
+            s = str(
+                self.__process.readAllStandardError(),
+                Preferences.getSystem("IOEncoding"),
+                "replace",
+            )
+            self.errorGroup.show()
+            self.errors.insertPlainText(s)
+            self.errors.ensureCursorVisible()
+
+            QCoreApplication.processEvents()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxAppStartDialog.ui	Mon Jun 24 19:48:46 2024 +0200
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PipxAppStartDialog</class>
+ <widget class="QDialog" name="PipxAppStartDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>700</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string notr="true">Start Application</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QLabel" name="appLabel"/>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Command Line Parameters</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <widget class="QLineEdit" name="parametersEdit">
+        <property name="toolTip">
+         <string>Enter the command line parameters for the application.</string>
+        </property>
+        <property name="clearButtonEnabled">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QToolButton" name="executeButton">
+        <property name="toolTip">
+         <string>Press to execute the application with the entered parameters.</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="outputGroup">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>3</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Output</string>
+     </property>
+     <layout class="QVBoxLayout">
+      <item>
+       <widget class="QTextEdit" name="resultbox">
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+        <property name="acceptRichText">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="errorGroup">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>1</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Errors</string>
+     </property>
+     <layout class="QVBoxLayout">
+      <item>
+       <widget class="QTextEdit" name="errors">
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+        <property name="acceptRichText">
+         <bool>false</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>
+ <layoutdefault spacing="6" margin="11"/>
+ <pixmapfunction>qPixmapFromMimeSource</pixmapfunction>
+ <tabstops>
+  <tabstop>parametersEdit</tabstop>
+  <tabstop>executeButton</tabstop>
+  <tabstop>resultbox</tabstop>
+  <tabstop>errors</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxDialog.py	Mon Jun 24 19:48:46 2024 +0200
@@ -0,0 +1,217 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015 - 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog showing the output of a pip command.
+"""
+
+from PyQt6.QtCore import QCoreApplication, QProcess, Qt, QTimer, pyqtSlot
+from PyQt6.QtWidgets import QAbstractButton, QDialog, QDialogButtonBox
+
+from eric7 import Preferences
+from eric7.EricWidgets import EricMessageBox
+
+from .Ui_PipxDialog import Ui_PipxDialog
+
+
+class PipDialog(QDialog, Ui_PipxDialog):
+    """
+    Class implementing a dialog showing the output of a 'python -m pip'
+    command.
+    """
+
+    def __init__(self, text, parent=None):
+        """
+        Constructor
+
+        @param text text to be shown by the label
+        @type str
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)
+
+        self.proc = None
+        self.__processQueue = []
+
+        self.outputGroup.setTitle(text)
+
+        self.show()
+        QCoreApplication.processEvents()
+
+    def closeEvent(self, e):
+        """
+        Protected slot implementing a close event handler.
+
+        @param e close event
+        @type QCloseEvent
+        """
+        self.__cancel()
+        e.accept()
+
+    def __finish(self):
+        """
+        Private slot called when the process finished or the user pressed
+        the button.
+        """
+        if (
+            self.proc is not None
+            and self.proc.state() != QProcess.ProcessState.NotRunning
+        ):
+            self.proc.terminate()
+            QTimer.singleShot(2000, self.proc.kill)
+            self.proc.waitForFinished(3000)
+
+        self.proc = None
+
+        if self.__processQueue:
+            command, args = self.__processQueue.pop(0)
+            self.__addOutput("\n\n")
+            self.startProcess(command, args)
+        else:
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(
+                True
+            )
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(
+                False
+            )
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(
+                True
+            )
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocus(
+                Qt.FocusReason.OtherFocusReason
+            )
+
+    def __cancel(self):
+        """
+        Private slot to cancel the current action.
+        """
+        self.__processQueue = []
+        self.__finish()
+
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot called by a button of the button box clicked.
+
+        @param button button that was clicked
+        @type QAbstractButton
+        """
+        if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close):
+            self.close()
+        elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel):
+            self.__cancel()
+
+    @pyqtSlot(int, QProcess.ExitStatus)
+    def __procFinished(self, _exitCode, _exitStatus):
+        """
+        Private slot connected to the finished signal.
+
+        @param _exitCode exit code of the process (unused)
+        @type int
+        @param _exitStatus exit status of the process (unused)
+        @type QProcess.ExitStatus
+        """
+        self.__finish()
+
+    def startProcess(self, pipx, args, showArgs=True):
+        """
+        Public slot used to start the process.
+
+        @param pipx path to the 'pipx' executable to be used
+        @type str
+        @param args list of arguments for the process
+        @type list of str
+        @param showArgs flag indicating to show the arguments
+        @type bool
+        @return flag indicating a successful start of the process
+        @rtype bool
+        """
+        if len(self.errors.toPlainText()) == 0:
+            self.errorGroup.hide()
+
+        if showArgs:
+            self.resultbox.append(pipx + " " + " ".join(args))
+            self.resultbox.append("")
+
+        self.proc = QProcess()
+        self.proc.finished.connect(self.__procFinished)
+        self.proc.readyReadStandardOutput.connect(self.__readStdout)
+        self.proc.readyReadStandardError.connect(self.__readStderr)
+        self.proc.start(pipx, args)
+        procStarted = self.proc.waitForStarted(5000)
+        if not procStarted:
+            self.buttonBox.setFocus()
+            EricMessageBox.critical(
+                self,
+                self.tr("Process Generation Error"),
+                self.tr("The process {0} could not be started.").format(pipx),
+            )
+        return procStarted
+
+    def startProcesses(self, processParams):
+        """
+        Public method to issue a list of commands to be executed.
+
+        @param processParams list of tuples containing the command
+            and arguments
+        @type list of tuples of (str, list of str)
+        @return flag indicating a successful start of the first process
+        @rtype bool
+        """
+        if len(processParams) > 1:
+            for command, args in processParams[1:]:
+                self.__processQueue.append((command, args[:]))
+        command, args = processParams[0]
+        return self.startProcess(command, args)
+
+    def __readStdout(self):
+        """
+        Private slot to handle the readyReadStandardOutput signal.
+
+        It reads the output of the process, formats it and inserts it into
+        the contents pane.
+        """
+        if self.proc is not None:
+            txt = str(
+                self.proc.readAllStandardOutput(),
+                Preferences.getSystem("IOEncoding"),
+                "replace",
+            )
+            self.__addOutput(txt)
+
+    def __addOutput(self, txt):
+        """
+        Private method to add some text to the output pane.
+
+        @param txt text to be added
+        @type str
+        """
+        self.resultbox.insertPlainText(txt)
+        self.resultbox.ensureCursorVisible()
+        QCoreApplication.processEvents()
+
+    def __readStderr(self):
+        """
+        Private slot to handle the readyReadStandardError signal.
+
+        It reads the error output of the process and inserts it into the
+        error pane.
+        """
+        if self.proc is not None:
+            s = str(
+                self.proc.readAllStandardError(),
+                Preferences.getSystem("IOEncoding"),
+                "replace",
+            )
+            self.errorGroup.show()
+            self.errors.insertPlainText(s)
+            self.errors.ensureCursorVisible()
+
+            QCoreApplication.processEvents()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxDialog.ui	Mon Jun 24 19:48:46 2024 +0200
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PipxDialog</class>
+ <widget class="QDialog" name="PipxDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>500</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string notr="true">pipx</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout">
+   <item>
+    <widget class="QGroupBox" name="outputGroup">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>2</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Output</string>
+     </property>
+     <layout class="QVBoxLayout">
+      <item>
+       <widget class="QTextEdit" name="resultbox">
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+        <property name="acceptRichText">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="errorGroup">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>1</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Errors</string>
+     </property>
+     <layout class="QVBoxLayout">
+      <item>
+       <widget class="QTextEdit" name="errors">
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+        <property name="acceptRichText">
+         <bool>false</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>
+ <layoutdefault spacing="6" margin="11"/>
+ <pixmapfunction>qPixmapFromMimeSource</pixmapfunction>
+ <tabstops>
+  <tabstop>resultbox</tabstop>
+  <tabstop>errors</tabstop>
+  <tabstop>buttonBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
--- a/PipxInterface/PipxWidget.py	Mon Jun 24 17:13:32 2024 +0200
+++ b/PipxInterface/PipxWidget.py	Mon Jun 24 19:48:46 2024 +0200
@@ -13,6 +13,8 @@
 from eric7.EricGui import EricPixmapCache
 
 from .Pipx import Pipx
+from .PipxAppStartDialog import PipxAppStartDialog
+
 from .Ui_PipxWidget import Ui_PipxWidget
 
 
@@ -120,3 +122,18 @@
             if itm.text(PipxWidget.PackageColumn) in expandedPackages:
                 itm.setExpanded(True)
         self.__resizePackagesColumns()
+
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_packagesList_itemActivated(self, item, column):
+        """
+        Private slot to start the activated item, if it is not a top level one
+
+        @param item reference to the activated item
+        @type QTreeWidgetItem
+        @param column column number of the activation
+        @type int
+        """
+        if item.parent() is not None:
+            app = item.data(0, PipxWidget.AppPathRole)
+            dlg = PipxAppStartDialog(app, self)
+            dlg.show()
--- a/PluginPipxInterface.epj	Mon Jun 24 17:13:32 2024 +0200
+++ b/PluginPipxInterface.epj	Mon Jun 24 19:48:46 2024 +0200
@@ -39,6 +39,7 @@
       "makefile": "OTHERS"
     },
     "FORMS": [
+      "PipxInterface/PipxDialog.ui",
       "PipxInterface/PipxWidget.ui"
     ],
     "HASH": "e670b8ea0fd5593abf0187483d113c50db352d90",
@@ -70,9 +71,26 @@
       "pyproject.toml"
     ],
     "OTHERTOOLSPARMS": {
+      "Black": {
+        "exclude": "/(\\.direnv|\\.eggs|\\.git|\\.hg|\\.ipynb_checkpoints|\\.mypy_cache|\\.nox|\\.pytest_cache|\\.ruff_cache|\\.tox|\\.svn|\\.venv|\\.vscode|__pypackages__|_build|buck-out|build|dist|venv)/",
+        "extend-exclude": "/(\n    Ui_.*\\.py\n)",
+        "force-exclude": "",
+        "line-length": 88,
+        "skip-magic-trailing-comma": false,
+        "skip-string-normalization": false,
+        "source": "project",
+        "target-version": [
+          "py313",
+          "py312",
+          "py311",
+          "py310",
+          "py39",
+          "py38"
+        ]
+      },
       "isort": {
         "combine_as_imports": true,
-        "config_source": "pyproject",
+        "config_source": "project",
         "extend_skip_glob": [
           "*/Ui_*.py"
         ],
@@ -106,7 +124,9 @@
     "RESOURCES": [],
     "SOURCES": [
       "PipxInterface/Pipx.py",
+      "PipxInterface/PipxDialog.py",
       "PipxInterface/PipxWidget.py",
+      "PipxInterface/Ui_PipxDialog.py",
       "PipxInterface/Ui_PipxWidget.py",
       "PipxInterface/__init__.py",
       "PluginPipxInterface.py",

eric ide

mercurial