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/>.
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"
58 MessageView::MessageView(QWidget
*parent
, QSettings
*settings
, Plugins::PluginManager
*pluginManager
,
59 Imap::Mailbox::FavoriteTagsModel
*m_favoriteTags
)
61 , m_stack(new QStackedLayout(this))
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);
80 connect(m_zoomIn
, &QAction::triggered
, this, &MessageView::zoomIn
);
82 m_zoomOut
= ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_out"), this);
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()
158 m_loadingItems
.clear();
159 message
= QModelIndex();
160 markAsReadTimer
->stop();
161 if (auto w
= bodyWidget()) {
162 m_stack
->removeWidget(dynamic_cast<QWidget
*>(w
));
165 m_envelope
->setMessage(QModelIndex());
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());
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().
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
227 m_loadingSpinner
->setText(tr("Waiting\nfor\nMessage..."));
228 m_loadingSpinner
->start();
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());
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
);
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
);
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
) {
288 m_waitingMessageConns
.clear();
291 void MessageView::markAsRead()
293 if (!message
.isValid())
295 Imap::Mailbox::Model
*model
= const_cast<Imap::Mailbox::Model
*>(dynamic_cast<const Imap::Mailbox::Model
*>(message
.model()));
297 if (!model
->isNetworkAvailable())
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) {
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
);
329 parentWidget()->setFocusPolicy(Qt::StrongFocus
);
332 } else if (event
->type() == QEvent::KeyPress
|| event
->type() == QEvent::KeyRelease
) {
333 QKeyEvent
*keyEvent
= static_cast<QKeyEvent
*>(event
);
334 switch (keyEvent
->key()) {
340 case Qt::Key_PageDown
:
341 MessageView::event(event
);
347 return QObject::eventFilter(object
, event
);
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
>();
360 if (!e
.from
.isEmpty())
361 sender
= e
.from
[0].prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME
);
362 if (e
.from
.isEmpty())
365 // One extra newline at the end of the quoted text to separate the response
368 return tr("On %1, %2 wrote:\n").arg(e
.date
.toLocalTime().toString(Qt::SystemLocaleLongDate
), sender
) + quote
.join(QStringLiteral("\n"));
373 #define FORWARD_METHOD(METHOD) \
374 void MessageView::METHOD() \
376 if (auto w = bodyWidget()) { \
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())
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
),
411 void MessageView::forward(MainWindow
*mainWindow
, const Composer::ForwardMode mode
)
413 if (!message
.isValid())
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
),
429 void MessageView::externalsRequested(const QUrl
&url
)
432 externalElements
->show();
435 void MessageView::enableExternalData()
437 netAccess
->setExternalsEnabled(true);
438 externalElements
->hide();
439 if (auto w
= bodyWidget()) {
444 void MessageView::newLabelAction(const QString
&tag
)
446 if (!message
.isValid())
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())
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())) {
479 w
->buildContextMenu(point
, menu
);
480 menu
.exec(w
->mapToGlobal(point
));
484 void MessageView::partLinkHovered(const QString
&link
, const QString
&title
, const QString
&textContent
)
487 Q_UNUSED(textContent
);
488 emit
linkHovered(link
);
491 QModelIndex
MessageView::currentMessage() const
496 void MessageView::onWebViewLoadStarted()
498 QWebView
*wv
= qobject_cast
<QWebView
*>(sender());
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());
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
);