SVN_SILENT made messages (.desktop file) - always resolve ours
[trojita.git] / src / Gui / MessageView.cpp
blob7d6493081bac53af343bd4787689337202806407
1 /* Copyright (C) 2006 - 2016 Jan Kundrát <jkt@kde.org>
2 Copyright (C) 2014 Luke Dashjr <luke+trojita@dashjr.org>
3 Copyright (C) 2014 - 2015 Stephan Platz <trojita@paalsteek.de>
5 This file is part of the Trojita Qt IMAP e-mail client,
6 http://trojita.flaska.net/
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License as
10 published by the Free Software Foundation; either version 2 of
11 the License or (at your option) version 3 or any later version
12 accepted by the membership of KDE e.V. (or its successor approved
13 by the membership of KDE e.V.), which shall act as a proxy
14 defined in Section 14 of version 3 of the license.
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with this program. If not, see <http://www.gnu.org/licenses/>.
25 #include <QKeyEvent>
26 #include <QMenu>
27 #include <QSettings>
28 #include <QStackedLayout>
30 #include "Common/InvokeMethod.h"
31 #include "Common/SettingsNames.h"
32 #include "Composer/QuoteText.h"
33 #include "Composer/SubjectMangling.h"
34 #include "Cryptography/MessageModel.h"
35 #include "Gui/MessageView.h"
36 #include "Gui/ComposeWidget.h"
37 #include "Gui/EnvelopeView.h"
38 #include "Gui/ExternalElementsWidget.h"
39 #include "Gui/OverlayWidget.h"
40 #include "Gui/PartWidgetFactoryVisitor.h"
41 #include "Gui/ShortcutHandler/ShortcutHandler.h"
42 #include "Gui/SimplePartWidget.h"
43 #include "Gui/Spinner.h"
44 #include "Gui/TagListWidget.h"
45 #include "Gui/UserAgentWebPage.h"
46 #include "Gui/Window.h"
47 #include "Imap/Model/MailboxTree.h"
48 #include "Imap/Model/MsgListModel.h"
49 #include "Imap/Model/NetworkWatcher.h"
50 #include "Imap/Model/Utils.h"
51 #include "Imap/Network/MsgPartNetAccessManager.h"
52 #include "Plugins/PluginManager.h"
53 #include "UiUtils/IconLoader.h"
55 namespace Gui
58 MessageView::MessageView(QWidget *parent, QSettings *settings, Plugins::PluginManager *pluginManager,
59 Imap::Mailbox::FavoriteTagsModel *m_favoriteTags)
60 : QWidget(parent)
61 , m_stack(new QStackedLayout(this))
62 , messageModel(0)
63 , netAccess(new Imap::Network::MsgPartNetAccessManager(this))
64 , factory(new PartWidgetFactory(netAccess, this,
65 std::unique_ptr<PartWidgetFactoryVisitor>(new PartWidgetFactoryVisitor())))
66 , m_settings(settings)
67 , m_pluginManager(pluginManager)
69 connect(netAccess, &Imap::Network::MsgPartNetAccessManager::requestingExternal, this, &MessageView::externalsRequested);
72 setBackgroundRole(QPalette::Base);
73 setForegroundRole(QPalette::Text);
74 setAutoFillBackground(true);
75 setFocusPolicy(Qt::StrongFocus); // not by the wheel
78 m_zoomIn = ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_in"), this);
79 addAction(m_zoomIn);
80 connect(m_zoomIn, &QAction::triggered, this, &MessageView::zoomIn);
82 m_zoomOut = ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_out"), this);
83 addAction(m_zoomOut);
84 connect(m_zoomOut, &QAction::triggered, this, &MessageView::zoomOut);
86 m_zoomOriginal = ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_original"), this);
87 addAction(m_zoomOriginal);
88 connect(m_zoomOriginal, &QAction::triggered, this, &MessageView::zoomOriginal);
90 m_findAction = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find")), tr("Search..."), this);
91 m_findAction->setShortcut(tr("Ctrl+F"));
92 // We can search only in one part of the message at a time. If searching is initiated by shortcut,
93 // there is no better solution than ask Part Widgets in visible order to handle this request. A
94 // callback method called MessageView::triggerSearchDialogBy is provided for this purpose.
95 // Other parts are searchable via the context menu only.
96 connect(m_findAction, &QAction::triggered, this, &MessageView::searchDialogRequested);
97 addAction(m_findAction);
99 // The homepage widget -- our poor man's splashscreen
100 m_homePage = new EmbeddedWebView(this, new QNetworkAccessManager(this));
101 m_homePage->setFixedSize(450,300);
102 CALL_LATER_NOARG(m_homePage, handlePageLoadFinished);
103 m_homePage->setPage(new UserAgentWebPage(m_homePage));
104 m_homePage->installEventFilter(this);
105 m_homePage->setAutoFillBackground(false);
106 m_stack->addWidget(m_homePage);
109 // The actual widget for the actual message
110 m_messageWidget = new QWidget(this);
111 auto fullMsgLayout = new QVBoxLayout(m_messageWidget);
112 m_stack->addWidget(m_messageWidget);
114 m_envelope = new EnvelopeView(m_messageWidget, this);
115 fullMsgLayout->addWidget(m_envelope, 1);
117 tags = new TagListWidget(m_messageWidget, m_favoriteTags);
118 connect(tags, &TagListWidget::tagAdded, this, &MessageView::newLabelAction);
119 connect(tags, &TagListWidget::tagRemoved, this, &MessageView::deleteLabelAction);
120 fullMsgLayout->addWidget(tags, 3);
122 externalElements = new ExternalElementsWidget(this);
123 externalElements->hide();
124 connect(externalElements, &ExternalElementsWidget::loadingEnabled, this, &MessageView::enableExternalData);
125 fullMsgLayout->addWidget(externalElements, 1);
127 // put the actual messages into an extra horizontal view
128 // this allows us easy usage of the trailing stretch and also to indent the message a bit
129 m_msgLayout = new QHBoxLayout;
130 m_msgLayout->setContentsMargins(6,6,6,0);
131 fullMsgLayout->addLayout(m_msgLayout, 1);
132 // add a strong stretch to squeeze header and message to the top
133 // possibly passing a large stretch factor to the message could be enough...
134 fullMsgLayout->addStretch(1000);
137 markAsReadTimer = new QTimer(this);
138 markAsReadTimer->setSingleShot(true);
139 connect(markAsReadTimer, &QTimer::timeout, this, &MessageView::markAsRead);
141 m_loadingSpinner = new Spinner(this);
142 m_loadingSpinner->setText(tr("Fetching\nMessage"));
143 m_loadingSpinner->setType(Spinner::Sun);
146 MessageView::~MessageView()
148 // Redmine #496 -- the default order of destruction starts with our QNAM subclass which in turn takes care of all pending
149 // QNetworkReply instances created by that manager. When the destruction goes to the WebKit objects, they try to disconnect
150 // from the network replies which are however gone already. We can mitigate that by simply making sure that the destruction
151 // starts with the QWebView subclasses and only after that proceeds to the QNAM. Qt's default order leads to segfaults here.
152 unsetPreviousMessage();
155 void MessageView::unsetPreviousMessage()
157 clearWaitingConns();
158 m_loadingItems.clear();
159 message = QModelIndex();
160 markAsReadTimer->stop();
161 if (auto w = bodyWidget()) {
162 m_stack->removeWidget(dynamic_cast<QWidget *>(w));
163 delete w;
165 m_envelope->setMessage(QModelIndex());
166 delete messageModel;
167 messageModel = nullptr;
170 void MessageView::setEmpty()
172 unsetPreviousMessage();
173 m_loadingSpinner->stop();
174 m_stack->setCurrentWidget(m_homePage);
175 emit messageChanged();
178 AbstractPartWidget *MessageView::bodyWidget() const
180 if (m_msgLayout->itemAt(0) && m_msgLayout->itemAt(0)->widget()) {
181 return dynamic_cast<AbstractPartWidget *>(m_msgLayout->itemAt(0)->widget());
182 } else {
183 return nullptr;
187 void MessageView::setMessage(const QModelIndex &index)
189 Q_ASSERT(index.isValid());
190 QModelIndex messageIndex = Imap::deproxifiedIndex(index);
191 Q_ASSERT(messageIndex.isValid());
193 if (message == messageIndex) {
194 // This is a duplicate call, let's do nothing.
195 // It might not be our fat-fingered user, but also just a side-effect of our duplicate invocation through
196 // QAbstractItemView::clicked() and activated().
197 return;
200 unsetPreviousMessage();
202 message = messageIndex;
203 messageModel = new Cryptography::MessageModel(this, message);
204 messageModel->setObjectName(QStringLiteral("cryptoMessageModel-%1-%2")
205 .arg(message.data(Imap::Mailbox::RoleMailboxName).toString(),
206 message.data(Imap::Mailbox::RoleMessageUid).toString()));
207 for (const auto &module: m_pluginManager->mimePartReplacers()) {
208 messageModel->registerPartHandler(module);
210 emit messageModelChanged(messageModel);
212 // The data might be available from the local cache, so let's try to save a possible roundtrip here
213 // by explicitly requesting the data
214 message.data(Imap::Mailbox::RolePartData);
216 if (!message.data(Imap::Mailbox::RoleIsFetched).toBool()) {
217 // This happens when the message placeholder is already available in the GUI, but the actual message data haven't been
218 // loaded yet. This is especially common with the threading model, but also with bigger unsynced mailboxes.
219 // Note that the data might be already available in the cache, it's just that it isn't in the mailbox tree yet.
220 m_waitingMessageConns.emplace_back(
221 connect(messageModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft){
222 if (topLeft.data(Imap::Mailbox::RoleIsFetched).toBool()) {
223 // OK, message is fully fetched now
224 showMessageNow();
226 }));
227 m_loadingSpinner->setText(tr("Waiting\nfor\nMessage..."));
228 m_loadingSpinner->start();
229 } else {
230 showMessageNow();
234 /** @short Implementation of the "hey, let's really display the message, its BODYSTRUCTURE is available now" */
235 void MessageView::showMessageNow()
237 Q_ASSERT(message.data(Imap::Mailbox::RoleIsFetched).toBool());
239 clearWaitingConns();
241 QModelIndex rootPartIndex = messageModel->index(0,0);
242 Q_ASSERT(rootPartIndex.child(0,0).isValid());
244 netAccess->setExternalsEnabled(false);
245 externalElements->hide();
247 netAccess->setModelMessage(rootPartIndex);
249 m_loadingItems.clear();
250 m_loadingSpinner->stop();
252 m_envelope->setMessage(message);
254 auto updateTagList = [this]() {
255 tags->setTagList(message.data(Imap::Mailbox::RoleMessageFlags).toStringList());
257 connect(messageModel, &QAbstractItemModel::dataChanged, this, updateTagList);
258 updateTagList();
260 UiUtils::PartLoadingOptions loadingMode;
261 if (m_settings->value(Common::SettingsNames::guiPreferPlaintextRendering, QVariant(true)).toBool())
262 loadingMode |= UiUtils::PART_PREFER_PLAINTEXT_OVER_HTML;
263 auto viewer = factory->walk(rootPartIndex.child(0,0), 0, loadingMode);
264 viewer->setParent(this);
265 m_msgLayout->addWidget(viewer);
266 m_msgLayout->setAlignment(viewer, Qt::AlignTop|Qt::AlignLeft);
267 viewer->show();
268 // We want to propagate the QWheelEvent to upper layers
269 viewer->installEventFilter(this);
270 m_stack->setCurrentWidget(m_messageWidget);
272 if (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE
273 && m_settings->value(Common::SettingsNames::autoMarkReadEnabled, QVariant(true)).toBool()) {
274 // No additional delay is needed here because the MsgListView won't open a message while the user keeps scrolling,
275 // which was AFAIK the original intention
276 markAsReadTimer->start(m_settings->value(Common::SettingsNames::autoMarkReadSeconds, QVariant(0)).toUInt() * 1000);
279 emit messageChanged();
282 /** @short There's no point in waiting for the message to appear */
283 void MessageView::clearWaitingConns()
285 for (auto &conn: m_waitingMessageConns) {
286 disconnect(conn);
288 m_waitingMessageConns.clear();
291 void MessageView::markAsRead()
293 if (!message.isValid())
294 return;
295 Imap::Mailbox::Model *model = const_cast<Imap::Mailbox::Model *>(dynamic_cast<const Imap::Mailbox::Model *>(message.model()));
296 Q_ASSERT(model);
297 if (!model->isNetworkAvailable())
298 return;
299 if (!message.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool())
300 model->markMessagesRead(QModelIndexList() << message, Imap::Mailbox::FLAG_ADD);
303 /** @short Inhibit the automatic marking of the current message as already read
305 The user might have e.g. explicitly marked a previously read message as unread again immediately after navigating back to it
306 in the message listing. In that situation, the message viewer shall respect this decision and inhibit the helper which would
307 otherwise mark the current message as read after a short timeout.
309 void MessageView::stopAutoMarkAsRead()
311 markAsReadTimer->stop();
314 bool MessageView::eventFilter(QObject *object, QEvent *event)
316 if (event->type() == QEvent::Wheel) {
317 if (static_cast<QWheelEvent *>(event)->modifiers() == Qt::ControlModifier) {
318 if (static_cast<QWheelEvent *>(event)->delta() > 0) {
319 zoomIn();
320 } else {
321 zoomOut();
323 } else {
324 // while the containing scrollview has Qt::StrongFocus, the event forwarding breaks that
325 // -> completely disable focus for the following wheel event ...
326 parentWidget()->setFocusPolicy(Qt::NoFocus);
327 MessageView::event(event);
328 // ... set reset it
329 parentWidget()->setFocusPolicy(Qt::StrongFocus);
331 return true;
332 } else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
333 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
334 switch (keyEvent->key()) {
335 case Qt::Key_Left:
336 case Qt::Key_Right:
337 case Qt::Key_Up:
338 case Qt::Key_Down:
339 case Qt::Key_PageUp:
340 case Qt::Key_PageDown:
341 MessageView::event(event);
342 return true;
343 case Qt::Key_Home:
344 case Qt::Key_End:
345 return false;
346 default:
347 return QObject::eventFilter(object, event);
349 } else {
350 return QObject::eventFilter(object, event);
354 QString MessageView::quoteText() const
356 if (auto w = bodyWidget()) {
357 QStringList quote = Composer::quoteText(w->quoteMe().split(QLatin1Char('\n')));
358 const Imap::Message::Envelope &e = message.data(Imap::Mailbox::RoleMessageEnvelope).value<Imap::Message::Envelope>();
359 QString sender;
360 if (!e.from.isEmpty())
361 sender = e.from[0].prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME);
362 if (e.from.isEmpty())
363 sender = tr("you");
365 // One extra newline at the end of the quoted text to separate the response
366 quote << QString();
368 return tr("On %1, %2 wrote:\n").arg(e.date.toLocalTime().toString(Qt::SystemLocaleLongDate), sender) + quote.join(QStringLiteral("\n"));
370 return QString();
373 #define FORWARD_METHOD(METHOD) \
374 void MessageView::METHOD() \
376 if (auto w = bodyWidget()) { \
377 w->METHOD(); \
380 FORWARD_METHOD(zoomIn)
381 FORWARD_METHOD(zoomOut)
382 FORWARD_METHOD(zoomOriginal)
383 FORWARD_METHOD(searchDialogRequested)
385 void MessageView::setNetworkWatcher(Imap::Mailbox::NetworkWatcher *netWatcher)
387 m_netWatcher = netWatcher;
388 factory->setNetworkWatcher(netWatcher);
391 void MessageView::reply(MainWindow *mainWindow, Composer::ReplyMode mode)
393 if (!message.isValid())
394 return;
396 // The Message-Id of the original message might have been empty; be sure we can handle that
397 QByteArray messageId = message.data(Imap::Mailbox::RoleMessageMessageId).toByteArray();
398 QList<QByteArray> messageIdList;
399 if (!messageId.isEmpty()) {
400 messageIdList.append(messageId);
403 ComposeWidget::warnIfMsaNotConfigured(
404 ComposeWidget::createReply(mainWindow, mode, message, QList<QPair<Composer::RecipientKind,QString> >(),
405 Composer::Util::replySubject(message.data(Imap::Mailbox::RoleMessageSubject).toString()),
406 quoteText(), messageIdList,
407 message.data(Imap::Mailbox::RoleMessageHeaderReferences).value<QList<QByteArray> >() + messageIdList),
408 mainWindow);
411 void MessageView::forward(MainWindow *mainWindow, const Composer::ForwardMode mode)
413 if (!message.isValid())
414 return;
416 // The Message-Id of the original message might have been empty; be sure we can handle that
417 QByteArray messageId = message.data(Imap::Mailbox::RoleMessageMessageId).toByteArray();
418 QList<QByteArray> messageIdList;
419 if (!messageId.isEmpty()) {
420 messageIdList.append(messageId);
423 ComposeWidget::warnIfMsaNotConfigured(
424 ComposeWidget::createForward(mainWindow, mode, message, Composer::Util::forwardSubject(message.data(Imap::Mailbox::RoleMessageSubject).toString()),
425 messageIdList, message.data(Imap::Mailbox::RoleMessageHeaderReferences).value<QList<QByteArray>>() + messageIdList),
426 mainWindow);
429 void MessageView::externalsRequested(const QUrl &url)
431 Q_UNUSED(url);
432 externalElements->show();
435 void MessageView::enableExternalData()
437 netAccess->setExternalsEnabled(true);
438 externalElements->hide();
439 if (auto w = bodyWidget()) {
440 w->reloadContents();
444 void MessageView::newLabelAction(const QString &tag)
446 if (!message.isValid())
447 return;
449 Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
450 model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_ADD);
453 void MessageView::deleteLabelAction(const QString &tag)
455 if (!message.isValid())
456 return;
458 Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
459 model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_REMOVE);
462 void MessageView::setHomepageUrl(const QUrl &homepage)
464 m_homePage->load(homepage);
467 void MessageView::showEvent(QShowEvent *se)
469 QWidget::showEvent(se);
470 // The Oxygen style reset the attribute - since we're gonna cause an update() here anyway, it's
471 // a good moment to stress that "we know better, Hugo ;-)" -- Thomas
472 setAutoFillBackground(true);
475 void MessageView::partContextMenuRequested(const QPoint &point)
477 if (SimplePartWidget *w = qobject_cast<SimplePartWidget *>(sender())) {
478 QMenu menu(w);
479 w->buildContextMenu(point, menu);
480 menu.exec(w->mapToGlobal(point));
484 void MessageView::partLinkHovered(const QString &link, const QString &title, const QString &textContent)
486 Q_UNUSED(title);
487 Q_UNUSED(textContent);
488 emit linkHovered(link);
491 QModelIndex MessageView::currentMessage() const
493 return message;
496 void MessageView::onWebViewLoadStarted()
498 QWebView *wv = qobject_cast<QWebView*>(sender());
499 Q_ASSERT(wv);
501 if (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE) {
502 m_loadingItems << wv;
503 m_loadingSpinner->start(250);
507 void MessageView::onWebViewLoadFinished()
509 QWebView *wv = qobject_cast<QWebView*>(sender());
510 Q_ASSERT(wv);
511 m_loadingItems.remove(wv);
512 if (m_loadingItems.isEmpty())
513 m_loadingSpinner->stop();
516 Plugins::PluginManager *MessageView::pluginManager() const
518 return m_pluginManager;
521 /** @short Callback for AbstractPartWidget */
522 void MessageView::triggerSearchDialogBy(EmbeddedWebView *w)
524 emit searchRequestedBy(w);