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