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
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;
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
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)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
virtual bool event(QEvent *e)
T qobject_cast(QObject *object)
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
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 Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.