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
32#include <chrono>
33
34using namespace std::chrono_literals;
35
36using namespace MessageComposer;
37
38EditorWatcher::EditorWatcher(const QUrl &url, const QString &mimeType, OpenWithOption option, QObject *parent, QWidget *parentWidget)
40 , mUrl(url)
41 , mMimeType(mimeType)
42 , mParentWidget(parentWidget)
43 , mOpenWithOption(option)
44{
45 assert(mUrl.isLocalFile());
46 mTimer.setSingleShot(true);
47 connect(&mTimer, &QTimer::timeout, this, &EditorWatcher::checkEditDone);
48}
49
50EditorWatcher::~EditorWatcher()
51{
52#ifdef HAVE_SYS_INOTIFY_H
53 ::close(mInotifyFd);
54#endif
55}
56
57EditorWatcher::ErrorEditorWatcher EditorWatcher::start()
58{
59 // find an editor
60 QList<QUrl> list;
61 list.append(mUrl);
63 if ((mOpenWithOption == OpenWithDialog) || !offer) {
64 std::unique_ptr<KOpenWithDialog> dlg(new KOpenWithDialog(list, i18n("Edit with:"), QString(), mParentWidget));
65 const int dlgrc = dlg->exec();
66 if (dlgrc) {
67 offer = dlg->service();
68 }
69 if (!dlgrc) {
70 return Canceled;
71 }
72 if (!offer) {
73 return NoServiceFound;
74 }
75 }
76
77#ifdef HAVE_SYS_INOTIFY_H
78 // monitor file
79 mInotifyFd = inotify_init();
80 if (mInotifyFd > 0) {
81 (void)fcntl(mInotifyFd, F_SETFD, FD_CLOEXEC);
82 mInotifyWatch = inotify_add_watch(mInotifyFd, mUrl.path().toLatin1().constData(), IN_CLOSE | IN_OPEN | IN_MODIFY | IN_ATTRIB);
83 if (mInotifyWatch >= 0) {
84 auto sn = new QSocketNotifier(mInotifyFd, QSocketNotifier::Read, this);
85 connect(sn, &QSocketNotifier::activated, this, &EditorWatcher::inotifyEvent);
86 mHaveInotify = true;
87 mFileModified = false;
88 }
89 } else {
90 qCWarning(MESSAGECOMPOSER_LOG()) << "Failed to activate INOTIFY!";
91 }
92#endif
93
94 // start the editor
95 KIO::DesktopExecParser parser(*offer, list);
96 parser.setUrlsAreTempFiles(false);
97 const QStringList params = parser.resultingArguments();
98 mEditor = new KProcess(this);
99 mEditor->setProgram(params);
100 connect(mEditor, &KProcess::finished, this, &EditorWatcher::editorExited);
101 mEditor->start();
102 if (!mEditor->waitForStarted()) {
103 return CannotStart;
104 }
105 mEditorRunning = true;
106
107 mEditTime.start();
108 return NoError;
109}
110
111bool EditorWatcher::fileChanged() const
112{
113 return mFileModified;
114}
115
116QUrl EditorWatcher::url() const
117{
118 return mUrl;
119}
120
121void EditorWatcher::inotifyEvent()
122{
123 assert(mHaveInotify);
124
125#ifdef HAVE_SYS_INOTIFY_H
126 int pending = -1;
127 int offsetStartRead = 0; // where we read into buffer
128 char buf[8192];
129 assert(mInotifyFd > -1);
130 ioctl(mInotifyFd, FIONREAD, &pending);
131
132 while (pending > 0) {
133 const int bytesToRead = qMin(pending, (int)sizeof(buf) - offsetStartRead);
134
135 int bytesAvailable = read(mInotifyFd, &buf[offsetStartRead], bytesToRead);
136 pending -= bytesAvailable;
137 bytesAvailable += offsetStartRead;
138 offsetStartRead = 0;
139
140 int offsetCurrent = 0;
141 while (bytesAvailable >= (int)sizeof(struct inotify_event)) {
142 const struct inotify_event *const event = (struct inotify_event *)&buf[offsetCurrent];
143 const int eventSize = sizeof(struct inotify_event) + event->len;
144 if (bytesAvailable < eventSize) {
145 break;
146 }
147
148 bytesAvailable -= eventSize;
149 offsetCurrent += eventSize;
150 if (event->mask & IN_OPEN) {
151 mFileOpen = true;
152 }
153 if (event->mask & IN_CLOSE) {
154 mFileOpen = false;
155 }
156 if (event->mask & (IN_MODIFY | IN_ATTRIB)) {
157 mFileModified = true;
158 }
159 }
160 if (bytesAvailable > 0) {
161 // copy partial event to beginning of buffer
162 memmove(buf, &buf[offsetCurrent], bytesAvailable);
163 offsetStartRead = bytesAvailable;
164 }
165 }
166#endif
167 mTimer.start(500ms);
168}
169
170void EditorWatcher::editorExited()
171{
172 mEditorRunning = false;
173 mTimer.start(500ms);
174}
175
176void EditorWatcher::checkEditDone()
177{
178 if (mEditorRunning || (mFileOpen && mHaveInotify) || mDone) {
179 return;
180 }
181
182 static QStringList readOnlyMimeTypes;
183 if (readOnlyMimeTypes.isEmpty()) {
184 readOnlyMimeTypes << QStringLiteral("message/rfc822") << QStringLiteral("application/pdf");
185 }
186
187 // protect us against double-deletion by calling this method again while
188 // the subeventloop of the message box is running
189 mDone = true;
190
191 // check if it's a mime type that's mostly handled read-only
192 const bool isReadOnlyMimeType = (readOnlyMimeTypes.contains(mMimeType) || mMimeType.startsWith(QLatin1StringView("image/")));
193
194 // nobody can edit that fast, we seem to be unable to detect
195 // when the editor will be closed
196 if (mEditTime.elapsed() <= 3000 && !isReadOnlyMimeType) {
197 KMessageBox::information(mParentWidget,
198 i18n("KMail is unable to detect when the chosen editor is closed. "
199 "To avoid data loss, editing the attachment will be aborted."),
200 i18nc("@title:window", "Unable to edit attachment"),
201 QStringLiteral("UnableToEditAttachment"));
202 }
203
204 Q_EMIT editDone(this);
205 deleteLater();
206}
207
208#include "moc_editorwatcher.cpp"
QExplicitlySharedDataPointer< KService > Ptr
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...
void append(QList< T > &&value)
bool isEmpty() const const
QObject(QObject *parent)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
virtual bool event(QEvent *e)
QObject * parent() const const
void finished(int exitCode, QProcess::ExitStatus exitStatus)
void activated(QSocketDescriptor socket, QSocketNotifier::Type type)
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 31 2025 12:05:40 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.