File indexing completed on 2024-05-19 05:21:45

0001 /*
0002    SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "richtextexternalcomposer.h"
0008 #include "richtextcomposer.h"
0009 
0010 #include <KLocalizedString>
0011 #include <KMacroExpander>
0012 #include <KMessageBox>
0013 #include <KProcess>
0014 #include <KShell>
0015 #include <QTemporaryFile>
0016 
0017 using namespace KPIMTextEdit;
0018 
0019 class Q_DECL_HIDDEN RichTextExternalComposer::RichTextExternalComposerPrivate
0020 {
0021 public:
0022     RichTextExternalComposerPrivate(RichTextComposer *composer)
0023         : richTextComposer(composer)
0024     {
0025     }
0026 
0027     void cannotStartProcess(const QString &commandLine);
0028     QString extEditorPath;
0029     KProcess *externalEditorProcess = nullptr;
0030     QTemporaryFile *extEditorTempFile = nullptr;
0031     RichTextComposer *const richTextComposer;
0032     bool useExtEditor = false;
0033 };
0034 
0035 RichTextExternalComposer::RichTextExternalComposer(RichTextComposer *composer, QObject *parent)
0036     : QObject(parent)
0037     , d(new RichTextExternalComposerPrivate(composer))
0038 {
0039 }
0040 
0041 RichTextExternalComposer::~RichTextExternalComposer() = default;
0042 
0043 bool RichTextExternalComposer::useExternalEditor() const
0044 {
0045     return d->useExtEditor;
0046 }
0047 
0048 void RichTextExternalComposer::setUseExternalEditor(bool value)
0049 {
0050     d->useExtEditor = value;
0051 }
0052 
0053 void RichTextExternalComposer::setExternalEditorPath(const QString &path)
0054 {
0055     d->extEditorPath = path;
0056 }
0057 
0058 QString RichTextExternalComposer::externalEditorPath() const
0059 {
0060     return d->extEditorPath;
0061 }
0062 
0063 void RichTextExternalComposer::startExternalEditor()
0064 {
0065     if (d->useExtEditor && !d->externalEditorProcess) {
0066         const QString commandLine = d->extEditorPath.trimmed();
0067         if (d->extEditorPath.isEmpty()) {
0068             setUseExternalEditor(false);
0069             KMessageBox::error(d->richTextComposer, i18n("Command line is empty. Please verify settings."), i18nc("@title:window", "Empty command line"));
0070             return;
0071         }
0072 
0073         d->extEditorTempFile = new QTemporaryFile();
0074         if (!d->extEditorTempFile->open()) {
0075             delete d->extEditorTempFile;
0076             d->extEditorTempFile = nullptr;
0077             setUseExternalEditor(false);
0078             return;
0079         }
0080 
0081         d->extEditorTempFile->write(d->richTextComposer->textOrHtml().toUtf8());
0082         d->extEditorTempFile->close();
0083 
0084         d->externalEditorProcess = new KProcess();
0085         // construct command line...
0086         QHash<QChar, QString> map;
0087         map.insert(QLatin1Char('l'), QString::number(d->richTextComposer->textCursor().blockNumber() + 1));
0088         map.insert(QLatin1Char('w'), QString::number(static_cast<qulonglong>(d->richTextComposer->winId())));
0089         map.insert(QLatin1Char('f'), d->extEditorTempFile->fileName());
0090         const QString cmd = KMacroExpander::expandMacrosShellQuote(commandLine, map);
0091         const QStringList arg = KShell::splitArgs(cmd);
0092         bool filenameAdded = false;
0093         if (commandLine.contains(QLatin1StringView("%f"))) {
0094             filenameAdded = true;
0095         }
0096         QStringList command;
0097         if (!arg.isEmpty()) {
0098             command << arg;
0099         }
0100         if (command.isEmpty()) {
0101             d->cannotStartProcess(commandLine);
0102             return;
0103         }
0104 
0105         (*d->externalEditorProcess) << command;
0106         if (!filenameAdded) { // no %f in the editor command
0107             (*d->externalEditorProcess) << d->extEditorTempFile->fileName();
0108         }
0109 
0110         connect(d->externalEditorProcess, &KProcess::finished, this, &RichTextExternalComposer::slotEditorFinished);
0111         d->externalEditorProcess->start();
0112         if (!d->externalEditorProcess->waitForStarted()) {
0113             d->cannotStartProcess(commandLine);
0114         } else {
0115             Q_EMIT externalEditorStarted();
0116         }
0117     }
0118 }
0119 
0120 void RichTextExternalComposer::RichTextExternalComposerPrivate::cannotStartProcess(const QString &commandLine)
0121 {
0122     KMessageBox::error(richTextComposer, i18n("External editor cannot be started. Please verify command \"%1\"", commandLine));
0123     richTextComposer->killExternalEditor();
0124     richTextComposer->setUseExternalEditor(false);
0125 }
0126 
0127 void RichTextExternalComposer::slotEditorFinished(int codeError, QProcess::ExitStatus exitStatus)
0128 {
0129     if (exitStatus == QProcess::NormalExit) {
0130         // the external editor could have renamed the original file and recreated a new file
0131         // with the given filename, so we need to reopen the file after the editor exited
0132         QFile localFile(d->extEditorTempFile->fileName());
0133         if (localFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
0134             const QByteArray f = localFile.readAll();
0135             d->richTextComposer->setTextOrHtml(QString::fromUtf8(f.data(), f.size()));
0136             d->richTextComposer->document()->setModified(true);
0137             localFile.close();
0138         }
0139         if (codeError > 0) {
0140             KMessageBox::error(d->richTextComposer, i18n("Error was found when we started external editor."), i18n("External Editor Closed"));
0141             setUseExternalEditor(false);
0142         }
0143         Q_EMIT externalEditorClosed();
0144     }
0145 
0146     killExternalEditor(); // cleanup...
0147 }
0148 
0149 bool RichTextExternalComposer::checkExternalEditorFinished()
0150 {
0151     if (!d->externalEditorProcess) {
0152         return true;
0153     }
0154 
0155     const int ret = KMessageBox::warningTwoActionsCancel(d->richTextComposer,
0156                                                          xi18nc("@info",
0157                                                                 "The external editor is still running.<nl/>"
0158                                                                 "Do you want to stop the editor or keep it running?<nl/>"
0159                                                                 "<warning>Stopping the editor will cause all your "
0160                                                                 "unsaved changes to be lost.</warning>"),
0161                                                          i18nc("@title:window", "External Editor Running"),
0162                                                          KGuiItem(i18nc("@action:button", "Stop Editor")),
0163                                                          KGuiItem(i18nc("@action:button", "Keep Editor Running")));
0164 
0165     switch (ret) {
0166     case KMessageBox::ButtonCode::PrimaryAction:
0167         killExternalEditor();
0168         return true;
0169     case KMessageBox::ButtonCode::SecondaryAction:
0170         return true;
0171     default:
0172         return false;
0173     }
0174 }
0175 
0176 void RichTextExternalComposer::killExternalEditor()
0177 {
0178     if (d->externalEditorProcess) {
0179         d->externalEditorProcess->deleteLater();
0180     }
0181     d->externalEditorProcess = nullptr;
0182     delete d->extEditorTempFile;
0183     d->extEditorTempFile = nullptr;
0184 }
0185 
0186 bool RichTextExternalComposer::isInProgress() const
0187 {
0188     return d->externalEditorProcess;
0189 }
0190 
0191 #include "moc_richtextexternalcomposer.cpp"