Messagelib

editorwatcher.cpp
1/*
2 SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "editorwatcher.h"
8#include <config-messagecomposer.h>
9
10#include "messagecomposer_debug.h"
11
12#include <KApplicationTrader>
13#include <KIO/DesktopExecParser>
14#include <KLocalizedString>
15#include <KMessageBox>
16#include <KOpenWithDialog>
17#include <KProcess>
18
19#include <QSocketNotifier>
20
21#include <cassert>
22#include <memory>
23
24// inotify stuff taken from kdelibs/kio/kio/kdirwatch.cpp
25#ifdef HAVE_SYS_INOTIFY_H
26#include <fcntl.h>
27#include <sys/inotify.h>
28#include <sys/ioctl.h>
29#include <unistd.h>
30#endif
31
32using namespace MessageComposer;
33
34EditorWatcher::EditorWatcher(const QUrl &url, const QString &mimeType, OpenWithOption option, QObject *parent, QWidget *parentWidget)
35 : QObject(parent)
36 , mUrl(url)
37 , mMimeType(mimeType)
38 , mParentWidget(parentWidget)
39 , mOpenWithOption(option)
40{
41 assert(mUrl.isLocalFile());
42 mTimer.setSingleShot(true);
43 connect(&mTimer, &QTimer::timeout, this, &EditorWatcher::checkEditDone);
44}
45
46EditorWatcher::~EditorWatcher()
47{
48#ifdef HAVE_SYS_INOTIFY_H
49 ::close(mInotifyFd);
50#endif
51}
52
53EditorWatcher::ErrorEditorWatcher EditorWatcher::start()
54{
55 // find an editor
56 QList<QUrl> list;
57 list.append(mUrl);
59 if ((mOpenWithOption == OpenWithDialog) || !offer) {
60 std::unique_ptr<KOpenWithDialog> dlg(new KOpenWithDialog(list, i18n("Edit with:"), QString(), mParentWidget));
61 const int dlgrc = dlg->exec();
62 if (dlgrc) {
63 offer = dlg->service();
64 }
65 if (!dlgrc) {
66 return Canceled;
67 }
68 if (!offer) {
69 return NoServiceFound;
70 }
71 }
72
73#ifdef HAVE_SYS_INOTIFY_H
74 // monitor file
75 mInotifyFd = inotify_init();
76 if (mInotifyFd > 0) {
77 (void)fcntl(mInotifyFd, F_SETFD, FD_CLOEXEC);
78 mInotifyWatch = inotify_add_watch(mInotifyFd, mUrl.path().toLatin1().constData(), IN_CLOSE | IN_OPEN | IN_MODIFY | IN_ATTRIB);
79 if (mInotifyWatch >= 0) {
80 auto sn = new QSocketNotifier(mInotifyFd, QSocketNotifier::Read, this);
81 connect(sn, &QSocketNotifier::activated, this, &EditorWatcher::inotifyEvent);
82 mHaveInotify = true;
83 mFileModified = false;
84 }
85 } else {
86 qCWarning(MESSAGECOMPOSER_LOG()) << "Failed to activate INOTIFY!";
87 }
88#endif
89
90 // start the editor
91 KIO::DesktopExecParser parser(*offer, list);
92 parser.setUrlsAreTempFiles(false);
93 const QStringList params = parser.resultingArguments();
94 mEditor = new KProcess(this);
95 mEditor->setProgram(params);
96 connect(mEditor, &KProcess::finished, this, &EditorWatcher::editorExited);
97 mEditor->start();
98 if (!mEditor->waitForStarted()) {
99 return CannotStart;
100 }
101 mEditorRunning = true;
102
103 mEditTime.start();
104 return NoError;
105}
106
107bool EditorWatcher::fileChanged() const
108{
109 return mFileModified;
110}
111
112QUrl EditorWatcher::url() const
113{
114 return mUrl;
115}
116
117void EditorWatcher::inotifyEvent()
118{
119 assert(mHaveInotify);
120
121#ifdef HAVE_SYS_INOTIFY_H
122 int pending = -1;
123 int offsetStartRead = 0; // where we read into buffer
124 char buf[8192];
125 assert(mInotifyFd > -1);
126 ioctl(mInotifyFd, FIONREAD, &pending);
127
128 while (pending > 0) {
129 const int bytesToRead = qMin(pending, (int)sizeof(buf) - offsetStartRead);
130
131 int bytesAvailable = read(mInotifyFd, &buf[offsetStartRead], bytesToRead);
132 pending -= bytesAvailable;
133 bytesAvailable += offsetStartRead;
134 offsetStartRead = 0;
135
136 int offsetCurrent = 0;
137 while (bytesAvailable >= (int)sizeof(struct inotify_event)) {
138 const struct inotify_event *const event = (struct inotify_event *)&buf[offsetCurrent];
139 const int eventSize = sizeof(struct inotify_event) + event->len;
140 if (bytesAvailable < eventSize) {
141 break;
142 }
143
144 bytesAvailable -= eventSize;
145 offsetCurrent += eventSize;
146 if (event->mask & IN_OPEN) {
147 mFileOpen = true;
148 }
149 if (event->mask & IN_CLOSE) {
150 mFileOpen = false;
151 }
152 if (event->mask & (IN_MODIFY | IN_ATTRIB)) {
153 mFileModified = true;
154 }
155 }
156 if (bytesAvailable > 0) {
157 // copy partial event to beginning of buffer
158 memmove(buf, &buf[offsetCurrent], bytesAvailable);
159 offsetStartRead = bytesAvailable;
160 }
161 }
162#endif
163 mTimer.start(500);
164}
165
166void EditorWatcher::editorExited()
167{
168 mEditorRunning = false;
169 mTimer.start(500);
170}
171
172void EditorWatcher::checkEditDone()
173{
174 if (mEditorRunning || (mFileOpen && mHaveInotify) || mDone) {
175 return;
176 }
177
178 static QStringList readOnlyMimeTypes;
179 if (readOnlyMimeTypes.isEmpty()) {
180 readOnlyMimeTypes << QStringLiteral("message/rfc822") << QStringLiteral("application/pdf");
181 }
182
183 // protect us against double-deletion by calling this method again while
184 // the subeventloop of the message box is running
185 mDone = true;
186
187 // check if it's a mime type that's mostly handled read-only
188 const bool isReadOnlyMimeType = (readOnlyMimeTypes.contains(mMimeType) || mMimeType.startsWith(QLatin1StringView("image/")));
189
190 // nobody can edit that fast, we seem to be unable to detect
191 // when the editor will be closed
192 if (mEditTime.elapsed() <= 3000 && !isReadOnlyMimeType) {
193 KMessageBox::information(mParentWidget,
194 i18n("KMail is unable to detect when the chosen editor is closed. "
195 "To avoid data loss, editing the attachment will be aborted."),
196 i18nc("@title:window", "Unable to edit attachment"),
197 QStringLiteral("UnableToEditAttachment"));
198 }
199
200 Q_EMIT editDone(this);
201 deleteLater();
202}
203
204#include "moc_editorwatcher.cpp"
void start()
void setProgram(const QString &exe, const QStringList &args=QStringList())
EditorWatcher(const QUrl &url, const QString &mimeType, OpenWithOption option, QObject *parent, QWidget *parentWidget)
Constructs an EditorWatcher.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KSERVICE_EXPORT KService::Ptr preferredService(const QString &mimeType)
QVariant read(const QByteArray &data, int versionOverride=0)
void information(QWidget *parent, const QString &text, const QString &title=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
const char * constData() const const
qint64 elapsed() const const
void append(QList< T > &&value)
bool isEmpty() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
virtual bool event(QEvent *e)
void finished(int exitCode, QProcess::ExitStatus exitStatus)
bool waitForStarted(int msecs)
void activated(QSocketDescriptor socket, QSocketNotifier::Type type)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
void setSingleShot(bool singleShot)
void start()
void timeout()
bool isLocalFile() const const
QString path(ComponentFormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:07:24 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.