ProjectDjango/DjangoDialog.py

Sun, 30 Mar 2025 15:25:10 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 30 Mar 2025 15:25:10 +0200
branch
eric7
changeset 206
4dfd8f6f2503
parent 197
2667e16a3379
permissions
-rw-r--r--

Created global tag <release-10.4.0>.

# -*- coding: utf-8 -*-

# Copyright (c) 2013 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog starting a process and showing its output.
"""

import os

from PyQt6.QtCore import QFileInfo, QProcess, QTimer, pyqtSlot
from PyQt6.QtWidgets import (
    QAbstractButton,
    QDialog,
    QDialogButtonBox,
    QLineEdit,
    QTextEdit,
)

from eric7 import Preferences
from eric7.EricWidgets import EricFileDialog, EricMessageBox
from eric7.Globals import strToQByteArray

try:
    from eric7.SystemUtilities.OSUtilities import isWindowsPlatform
except ImportError:
    # imports for eric < 23.1
    from eric7.Globals import isWindowsPlatform

from .Ui_DjangoDialog import Ui_DjangoDialog


class DjangoDialog(QDialog, Ui_DjangoDialog):
    """
    Class implementing a dialog starting a process and showing its output.

    It starts a QProcess and displays a dialog that
    shows the output of the process. The dialog is modal,
    which causes a synchronized execution of the process.
    """

    def __init__(
        self,
        text,
        fixed=False,
        linewrap=True,
        msgSuccess=None,
        msgError=None,
        saveFilters=None,
        showInput=False,
        parent=None,
    ):
        """
        Constructor

        @param text text to be shown by the label
        @type str
        @param fixed flag indicating a fixed font should be used
        @type bool
        @param linewrap flag indicating to wrap long lines
        @type bool
        @param msgSuccess optional string to show upon successful execution
        @type str
        @param msgError optional string to show upon unsuccessful execution
        @type str
        @param saveFilters filename filter string
        @type str
        @param showInput flag indicating to show the input widgets
        @type bool
        @param parent 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.buttonBox.button(QDialogButtonBox.StandardButton.Save).setEnabled(False)
        if saveFilters is None:
            self.buttonBox.button(QDialogButtonBox.StandardButton.Save).setHidden(True)

        self.ioEncoding = Preferences.getSystem("IOEncoding")

        self.proc = None
        self.argsLists = []
        self.workingDir = None
        self.mergedOutput = False
        self.msgSuccess = msgSuccess
        self.msgError = msgError
        self.fileFilters = saveFilters
        self.showInput = showInput
        self.intercept = False

        self.outputGroup.setTitle(text)

        if fixed:
            if isWindowsPlatform():
                self.resultbox.setFontFamily("Lucida Console")
            else:
                self.resultbox.setFontFamily("Monospace")

        if not linewrap:
            self.resultbox.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)

    @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.__finish()
        elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Save):
            self.__saveData()

    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.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.Save).setEnabled(True)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        self.proc = None

        if self.argsLists:
            args = self.argsLists[0][:]
            del self.argsLists[0]
            self.startProcess(args, self.workingDir, mergedOutput=self.mergedOutput)

    def __procFinished(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
        """
        self.normal = exitStatus == QProcess.ExitStatus.NormalExit and exitCode == 0
        self.__finish()

        if self.normal and self.msgSuccess:
            self.resultbox.insertPlainText(self.msgSuccess)
        elif not self.normal and self.msgError:
            self.resultbox.insertPlainText(self.msgError)
            self.errorGroup.show()
        self.resultbox.ensureCursorVisible()

    def startProcess(self, args, workingDir=None, showCommand=True, mergedOutput=False):
        """
        Public slot used to start the process.

        @param args list of arguments for the process
        @type list of str
        @param workingDir working directory for the process
        @type str
        @param showCommand flag indicating to show the command executed
        @type bool
        @param mergedOutput flag indicating to merge the output of the process
        @type bool
        @return flag indicating a successful start of the process
        @rtype bool
        """
        self.errorGroup.hide()

        self.normal = False

        self.proc = QProcess()
        if mergedOutput:
            self.proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)

        if showCommand:
            self.resultbox.append(" ".join(args))
            self.resultbox.append("")

        self.proc.finished.connect(self.__procFinished)
        self.proc.readyReadStandardOutput.connect(self.__readStdout)
        self.proc.readyReadStandardError.connect(self.__readStderr)

        if workingDir:
            self.proc.setWorkingDirectory(workingDir)
            self.workingDir = workingDir
        else:
            self.workingDir = ""

        prog = args[0]
        del args[0]
        self.proc.start(prog, args)
        procStarted = self.proc.waitForStarted()
        if not procStarted:
            self.buttonBox.setFocus()
            self.inputGroup.setEnabled(False)
            EricMessageBox.critical(
                self,
                self.tr("Process Generation Error"),
                self.tr(
                    "The process {0} could not be started. "
                    "Ensure, that it is in the search path."
                ).format(prog),
            )
        else:
            if self.showInput:
                self.inputGroup.setEnabled(True)
                self.inputGroup.show()
            else:
                self.inputGroup.setEnabled(False)
                self.inputGroup.hide()

        return procStarted

    def startBatchProcesses(self, argsLists, workingDir=None, mergedOutput=False):
        """
        Public slot used to start a batch of processes.

        @param argsLists list of lists of arguments for the processes
        @type list of list of str
        @param workingDir working directory for the process
        @type str
        @param mergedOutput flag indicating to merge the output of the process
        @type bool
        @return flag indicating a successful start of the first process
        @rtype bool
        """
        self.argsLists = argsLists[:]
        self.workingDir = workingDir
        self.mergedOutput = mergedOutput

        # start the first process
        args = self.argsLists[0][:]
        del self.argsLists[0]
        res = self.startProcess(args, self.workingDir, mergedOutput=self.mergedOutput)
        if not res:
            self.argsLists = []

        return res

    def normalExit(self):
        """
        Public method to check for a normal process termination.

        @return flag indicating normal process termination
        @rtype bool
        """
        return self.normal

    def normalExitWithoutErrors(self):
        """
        Public method to check for a normal process termination without
        error messages.

        @return flag indicating normal process termination
        @rtype bool
        """
        return self.normal and self.errors.toPlainText() == ""

    def __readStdout(self):
        """
        Private slot to handle the readyReadStdout signal.

        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.proc is not None:
            s = str(self.proc.readAllStandardOutput(), self.ioEncoding, "replace")
            self.resultbox.insertPlainText(s)
            self.resultbox.ensureCursorVisible()

    def __readStderr(self):
        """
        Private slot to handle the readyReadStderr signal.

        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.proc is not None:
            self.errorGroup.show()
            s = str(self.proc.readAllStandardError(), self.ioEncoding, "replace")
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()

    def __saveData(self):
        """
        Private slot to save the output to a file.
        """
        fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
            self, self.tr("Select data file"), self.workingDir, self.fileFilters, None
        )

        if fname:
            ext = QFileInfo(fname).suffix()
            if not ext:
                ex = selectedFilter.split("(*")[1].split(")")[0]
                if ex:
                    fname += ex

            txt = self.resultbox.toPlainText()

            try:
                with open(fname, "w", encoding="utf-8") as f:
                    f.write(txt)
            except OSError as err:
                EricMessageBox.critical(
                    self,
                    self.tr("Error saving data"),
                    self.tr(
                        """<p>The data could not be written"""
                        """ to <b>{0}</b></p><p>Reason: {1}</p>"""
                    ).format(fname, str(err)),
                )

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.

        @param isOn flag indicating the status of the check box
        @type bool
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.EchoMode.Password)
        else:
            self.input.setEchoMode(QLineEdit.EchoMode.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the manage.py process.
        """
        inputTxt = self.input.text()
        inputTxt += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(inputTxt)
            self.errors.ensureCursorVisible()

        self.proc.write(strToQByteArray(inputTxt))

        self.input.clear()
        self.passwordCheckBox.setChecked(False)

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.

        @param evt the key press event
        @type QKeyEvent
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return

        super().keyPressEvent(evt)

eric ide

mercurial