1 /*
2    SPDX-FileCopyrightText: 2015-2021 Laurent Montel <montel@kde.org>
3 
4    SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "richtextexternalcomposer.h"
8 #include "richtextcomposer.h"
9 
10 #include <KLocalizedString>
11 #include <KMacroExpander>
12 #include <KMessageBox>
13 #include <KProcess>
14 #include <KShell>
15 #include <QTemporaryFile>
16 
17 using namespace KPIMTextEdit;
18 
19 class Q_DECL_HIDDEN RichTextExternalComposer::RichTextExternalComposerPrivate
20 {
21 public:
RichTextExternalComposerPrivate(RichTextComposer * composer)22     RichTextExternalComposerPrivate(RichTextComposer *composer)
23         : richTextComposer(composer)
24     {
25     }
26 
27     void cannotStartProcess(const QString &commandLine);
28     QString extEditorPath;
29     KProcess *externalEditorProcess = nullptr;
30     QTemporaryFile *extEditorTempFile = nullptr;
31     RichTextComposer *const richTextComposer;
32     bool useExtEditor = false;
33 };
34 
RichTextExternalComposer(RichTextComposer * composer,QObject * parent)35 RichTextExternalComposer::RichTextExternalComposer(RichTextComposer *composer, QObject *parent)
36     : QObject(parent)
37     , d(new RichTextExternalComposerPrivate(composer))
38 {
39 }
40 
41 RichTextExternalComposer::~RichTextExternalComposer() = default;
42 
useExternalEditor() const43 bool RichTextExternalComposer::useExternalEditor() const
44 {
45     return d->useExtEditor;
46 }
47 
setUseExternalEditor(bool value)48 void RichTextExternalComposer::setUseExternalEditor(bool value)
49 {
50     d->useExtEditor = value;
51 }
52 
setExternalEditorPath(const QString & path)53 void RichTextExternalComposer::setExternalEditorPath(const QString &path)
54 {
55     d->extEditorPath = path;
56 }
57 
externalEditorPath() const58 QString RichTextExternalComposer::externalEditorPath() const
59 {
60     return d->extEditorPath;
61 }
62 
startExternalEditor()63 void RichTextExternalComposer::startExternalEditor()
64 {
65     if (d->useExtEditor && !d->externalEditorProcess) {
66         const QString commandLine = d->extEditorPath.trimmed();
67         if (d->extEditorPath.isEmpty()) {
68             setUseExternalEditor(false);
69             KMessageBox::error(d->richTextComposer, i18n("Command line is empty. Please verify settings."), i18n("Empty command line"));
70             return;
71         }
72 
73         d->extEditorTempFile = new QTemporaryFile();
74         if (!d->extEditorTempFile->open()) {
75             delete d->extEditorTempFile;
76             d->extEditorTempFile = nullptr;
77             setUseExternalEditor(false);
78             return;
79         }
80 
81         d->extEditorTempFile->write(d->richTextComposer->textOrHtml().toUtf8());
82         d->extEditorTempFile->close();
83 
84         d->externalEditorProcess = new KProcess();
85         // construct command line...
86         QHash<QChar, QString> map;
87         map.insert(QLatin1Char('l'), QString::number(d->richTextComposer->textCursor().blockNumber() + 1));
88         map.insert(QLatin1Char('w'), QString::number(static_cast<qulonglong>(d->richTextComposer->winId())));
89         map.insert(QLatin1Char('f'), d->extEditorTempFile->fileName());
90         const QString cmd = KMacroExpander::expandMacrosShellQuote(commandLine, map);
91         const QStringList arg = KShell::splitArgs(cmd);
92         bool filenameAdded = false;
93         if (commandLine.contains(QLatin1String("%f"))) {
94             filenameAdded = true;
95         }
96         QStringList command;
97         if (!arg.isEmpty()) {
98             command << arg;
99         }
100         if (command.isEmpty()) {
101             d->cannotStartProcess(commandLine);
102             return;
103         }
104 
105         (*d->externalEditorProcess) << command;
106         if (!filenameAdded) { // no %f in the editor command
107             (*d->externalEditorProcess) << d->extEditorTempFile->fileName();
108         }
109 
110         connect(d->externalEditorProcess, &KProcess::finished, this, &RichTextExternalComposer::slotEditorFinished);
111         d->externalEditorProcess->start();
112         if (!d->externalEditorProcess->waitForStarted()) {
113             d->cannotStartProcess(commandLine);
114         } else {
115             Q_EMIT externalEditorStarted();
116         }
117     }
118 }
119 
cannotStartProcess(const QString & commandLine)120 void RichTextExternalComposer::RichTextExternalComposerPrivate::cannotStartProcess(const QString &commandLine)
121 {
122     KMessageBox::error(richTextComposer, i18n("External editor cannot be started. Please verify command \"%1\"", commandLine));
123     richTextComposer->killExternalEditor();
124     richTextComposer->setUseExternalEditor(false);
125 }
126 
slotEditorFinished(int codeError,QProcess::ExitStatus exitStatus)127 void RichTextExternalComposer::slotEditorFinished(int codeError, QProcess::ExitStatus exitStatus)
128 {
129     if (exitStatus == QProcess::NormalExit) {
130         // the external editor could have renamed the original file and recreated a new file
131         // with the given filename, so we need to reopen the file after the editor exited
132         QFile localFile(d->extEditorTempFile->fileName());
133         if (localFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
134             const QByteArray f = localFile.readAll();
135             d->richTextComposer->setTextOrHtml(QString::fromUtf8(f.data(), f.size()));
136             d->richTextComposer->document()->setModified(true);
137             localFile.close();
138         }
139         if (codeError > 0) {
140             KMessageBox::error(d->richTextComposer, i18n("Error was found when we started external editor."), i18n("External Editor Closed"));
141             setUseExternalEditor(false);
142         }
143         Q_EMIT externalEditorClosed();
144     }
145 
146     killExternalEditor(); // cleanup...
147 }
148 
checkExternalEditorFinished()149 bool RichTextExternalComposer::checkExternalEditorFinished()
150 {
151     if (!d->externalEditorProcess) {
152         return true;
153     }
154 
155     const int ret = KMessageBox::warningYesNoCancel(d->richTextComposer,
156                                                     xi18nc("@info",
157                                                            "The external editor is still running.<nl/>"
158                                                            "Do you want to stop the editor or keep it running?<nl/>"
159                                                            "<warning>Stopping the editor will cause all your "
160                                                            "unsaved changes to be lost.</warning>"),
161                                                     i18nc("@title:window", "External Editor Running"),
162                                                     KGuiItem(i18nc("@action:button", "Stop Editor")),
163                                                     KGuiItem(i18nc("@action:button", "Keep Editor Running")));
164 
165     switch (ret) {
166     case KMessageBox::Yes:
167         killExternalEditor();
168         return true;
169     case KMessageBox::No:
170         return true;
171     default:
172         return false;
173     }
174 }
175 
killExternalEditor()176 void RichTextExternalComposer::killExternalEditor()
177 {
178     if (d->externalEditorProcess) {
179         d->externalEditorProcess->deleteLater();
180     }
181     d->externalEditorProcess = nullptr;
182     delete d->extEditorTempFile;
183     d->extEditorTempFile = nullptr;
184 }
185 
isInProgress() const186 bool RichTextExternalComposer::isInProgress() const
187 {
188     return d->externalEditorProcess;
189 }
190