Mon, 24 Jun 2024 19:48:46 +0200
Implemented two foundational dialogs for executing pipx commands and running applications from the list.
--- /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",