TopLeft-align messages in the viewport
[trojita.git] / src / Gui / MessageView.cpp
blob35c384de1516008f09d54e1e98fb1d68f51349e5
1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2 Copyright (C) 2014 Luke Dashjr <luke+trojita@dashjr.org>
4 This file is part of the Trojita Qt IMAP e-mail client,
5 http://trojita.flaska.net/
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License as
9 published by the Free Software Foundation; either version 2 of
10 the License or (at your option) version 3 or any later version
11 accepted by the membership of KDE e.V. (or its successor approved
12 by the membership of KDE e.V.), which shall act as a proxy
13 defined in Section 14 of version 3 of the license.
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
20 You should have received a copy of the GNU General Public License
21 along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #include <QDebug>
24 #include <QDesktopServices>
25 #include <QHeaderView>
26 #include <QKeyEvent>
27 #include <QMenu>
28 #include <QMessageBox>
29 #include <QProgressBar>
30 #include <QSettings>
31 #include <QTimer>
32 #include <QUrl>
33 #include <QVBoxLayout>
34 #include <QWebFrame>
35 #include <QWebHistory>
36 #include <QWebHitTestResult>
37 #include <QWebPage>
39 #include "MessageView.h"
40 #include "AbstractPartWidget.h"
41 #include "ComposeWidget.h"
42 #include "EmbeddedWebView.h"
43 #include "EnvelopeView.h"
44 #include "ExternalElementsWidget.h"
45 #include "OverlayWidget.h"
46 #include "PartWidgetFactoryVisitor.h"
47 #include "SimplePartWidget.h"
48 #include "Spinner.h"
49 #include "TagListWidget.h"
50 #include "UserAgentWebPage.h"
51 #include "Window.h"
52 #include "Common/InvokeMethod.h"
53 #include "Common/MetaTypes.h"
54 #include "Common/SettingsNames.h"
55 #include "Composer/QuoteText.h"
56 #include "Composer/SubjectMangling.h"
57 #include "Imap/Model/MailboxTree.h"
58 #include "Imap/Model/MsgListModel.h"
59 #include "Imap/Model/NetworkWatcher.h"
60 #include "Imap/Model/Utils.h"
61 #include "Imap/Network/MsgPartNetAccessManager.h"
63 namespace Gui
66 MessageView::MessageView(QWidget *parent, QSettings *settings): QWidget(parent), m_settings(settings)
68 QPalette pal = palette();
69 pal.setColor(backgroundRole(), palette().color(QPalette::Active, QPalette::Base));
70 pal.setColor(foregroundRole(), palette().color(QPalette::Active, QPalette::Text));
71 setPalette(pal);
72 setAutoFillBackground(true);
73 setFocusPolicy(Qt::StrongFocus); // not by the wheel
74 netAccess = new Imap::Network::MsgPartNetAccessManager(this);
75 connect(netAccess, SIGNAL(requestingExternal(QUrl)), this, SLOT(externalsRequested(QUrl)));
76 factory = new PartWidgetFactory(netAccess, this,
77 std::unique_ptr<PartWidgetFactoryVisitor>(new PartWidgetFactoryVisitor()));
79 emptyView = new EmbeddedWebView(this, new QNetworkAccessManager(this));
80 emptyView->setFixedSize(450,300);
81 CALL_LATER_NOARG(emptyView, handlePageLoadFinished);
82 emptyView->setPage(new UserAgentWebPage(emptyView));
83 emptyView->installEventFilter(this);
84 emptyView->setAutoFillBackground(false);
86 viewer = emptyView;
88 //BEGIN create header section
90 headerSection = new QWidget(this);
92 // we create a dummy header, pass it through the style and the use it's color roles so we
93 // know what headers in general look like in the system
94 QHeaderView helpingHeader(Qt::Horizontal);
95 helpingHeader.ensurePolished();
96 pal = headerSection->palette();
97 pal.setColor(headerSection->backgroundRole(), palette().color(QPalette::Active, helpingHeader.backgroundRole()));
98 pal.setColor(headerSection->foregroundRole(), palette().color(QPalette::Active, helpingHeader.foregroundRole()));
99 headerSection->setPalette(pal);
100 headerSection->setAutoFillBackground(true);
102 // the actual mail header
103 m_envelope = new EnvelopeView(headerSection, this);
105 // the tag bar
106 tags = new TagListWidget(headerSection);
107 tags->setBackgroundRole(helpingHeader.backgroundRole());
108 tags->setForegroundRole(helpingHeader.foregroundRole());
109 tags->hide();
110 connect(tags, SIGNAL(tagAdded(QString)), this, SLOT(newLabelAction(QString)));
111 connect(tags, SIGNAL(tagRemoved(QString)), this, SLOT(deleteLabelAction(QString)));
113 // whether we allow to load external elements
114 externalElements = new ExternalElementsWidget(this);
115 externalElements->hide();
116 connect(externalElements, SIGNAL(loadingEnabled()), this, SLOT(externalsEnabled()));
118 // layout the header
119 layout = new QVBoxLayout(headerSection);
120 layout->setSpacing(0);
121 layout->addWidget(m_envelope, 1);
122 layout->addWidget(tags, 3);
123 layout->addWidget(externalElements, 1);
125 //END create header section
127 //BEGIN layout the message
129 layout = new QVBoxLayout(this);
130 layout->setSpacing(0);
131 layout->setContentsMargins(0,0,0,0);
133 layout->addWidget(headerSection, 1);
135 headerSection->hide();
137 // put the actual messages into an extra horizontal view
138 // this allows us easy usage of the trailing stretch and also to indent the message a bit
139 QHBoxLayout *hLayout = new QHBoxLayout;
140 hLayout->setContentsMargins(6,6,6,0);
141 hLayout->addWidget(viewer);
142 static_cast<QVBoxLayout*>(layout)->addLayout(hLayout, 1);
143 // add a strong stretch to squeeze header and message to the top
144 // possibly passing a large stretch factor to the message could be enough...
145 layout->addStretch(1000);
147 //END layout the message
149 // make the layout used to add messages our new horizontal layout
150 layout = hLayout;
152 markAsReadTimer = new QTimer(this);
153 markAsReadTimer->setSingleShot(true);
154 connect(markAsReadTimer, SIGNAL(timeout()), this, SLOT(markAsRead()));
156 m_loadingSpinner = new Spinner(this);
157 m_loadingSpinner->setText(tr("Fetching\nMessage"));
158 m_loadingSpinner->setType(Spinner::Sun);
161 MessageView::~MessageView()
163 // Redmine #496 -- the default order of destruction starts with our QNAM subclass which in turn takes care of all pending
164 // QNetworkReply instances created by that manager. When the destruction goes to the WebKit objects, they try to disconnect
165 // from the network replies which are however gone already. We can mitigate that by simply making sure that the destruction
166 // starts with the QWebView subclasses and only after that proceeds to the QNAM. Qt's default order leads to segfaults here.
167 if (viewer != emptyView) {
168 delete viewer;
170 delete emptyView;
172 delete factory;
175 void MessageView::setEmpty()
177 markAsReadTimer->stop();
178 m_envelope->setMessage(QModelIndex());
179 headerSection->hide();
180 message = QModelIndex();
181 disconnect(this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
182 tags->hide();
183 if (viewer != emptyView) {
184 layout->removeWidget(viewer);
185 viewer->deleteLater();
186 viewer = emptyView;
187 viewer->show();
188 layout->addWidget(viewer);
189 emit messageChanged();
190 m_loadingItems.clear();
191 m_loadingSpinner->stop();
195 void MessageView::setMessage(const QModelIndex &index)
197 Q_ASSERT(index.isValid());
198 QModelIndex messageIndex = Imap::deproxifiedIndex(index);
199 Q_ASSERT(messageIndex.isValid());
201 // The data might be available from the local cache, so let's try to save a possible roundtrip here
202 // by explicitly requesting the data
203 messageIndex.data(Imap::Mailbox::RolePartData);
205 if (!messageIndex.data(Imap::Mailbox::RoleIsFetched).toBool()) {
206 // This happens when the message placeholder is already available in the GUI, but the actual message data haven't been
207 // loaded yet. This is especially common with the threading model.
208 // Note that the data might be already available in the cache, it's just that it isn't in the mailbox tree yet.
209 setEmpty();
210 connect(messageIndex.model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
211 message = messageIndex;
212 return;
215 QModelIndex rootPartIndex = messageIndex.child(0, 0);
217 headerSection->show();
218 if (message != messageIndex) {
219 emptyView->hide();
220 layout->removeWidget(viewer);
221 if (viewer != emptyView) {
222 viewer->setParent(0);
223 viewer->deleteLater();
225 message = messageIndex;
226 netAccess->setExternalsEnabled(false);
227 externalElements->hide();
229 netAccess->setModelMessage(message);
231 m_loadingItems.clear();
232 m_loadingSpinner->stop();
234 UiUtils::PartLoadingOptions loadingMode;
235 if (m_settings->value(Common::SettingsNames::guiPreferPlaintextRendering, QVariant(true)).toBool())
236 loadingMode |= UiUtils::PART_PREFER_PLAINTEXT_OVER_HTML;
237 viewer = factory->walk(rootPartIndex, 0, loadingMode);
238 viewer->setParent(this);
239 layout->addWidget(viewer);
240 layout->setAlignment(viewer, Qt::AlignTop|Qt::AlignLeft);
241 viewer->show();
242 m_envelope->setMessage(message);
244 tags->show();
245 tags->setTagList(messageIndex.data(Imap::Mailbox::RoleMessageFlags).toStringList());
246 disconnect(this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
247 connect(messageIndex.model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
249 emit messageChanged();
251 // We want to propagate the QWheelEvent to upper layers
252 viewer->installEventFilter(this);
255 if (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE
256 && m_settings->value(Common::SettingsNames::autoMarkReadEnabled, QVariant(true)).toBool()) {
257 // No additional delay is needed here because the MsgListView won't open a message while the user keeps scrolling,
258 // which was AFAIK the original intention
259 markAsReadTimer->start(m_settings->value(Common::SettingsNames::autoMarkReadSeconds, QVariant(0)).toUInt() * 1000);
263 void MessageView::markAsRead()
265 if (!message.isValid())
266 return;
267 Imap::Mailbox::Model *model = const_cast<Imap::Mailbox::Model *>(dynamic_cast<const Imap::Mailbox::Model *>(message.model()));
268 Q_ASSERT(model);
269 if (!model->isNetworkAvailable())
270 return;
271 if (!message.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool())
272 model->markMessagesRead(QModelIndexList() << message, Imap::Mailbox::FLAG_ADD);
275 /** @short Inhibit the automatic marking of the current message as already read
277 The user might have e.g. explicitly marked a previously read message as unread again immediately after navigating back to it
278 in the message listing. In that situation, the message viewer shall respect this decision and inhibit the helper which would
279 otherwise mark the current message as read after a short timeout.
281 void MessageView::stopAutoMarkAsRead()
283 markAsReadTimer->stop();
286 bool MessageView::eventFilter(QObject *object, QEvent *event)
288 if (event->type() == QEvent::Wheel) {
289 // while the containing scrollview has Qt::StrongFocus, the event forwarding breaks that
290 // -> completely disable focus for the following wheel event ...
291 parentWidget()->setFocusPolicy(Qt::NoFocus);
292 MessageView::event(event);
293 // ... set reset it
294 parentWidget()->setFocusPolicy(Qt::StrongFocus);
295 return true;
296 } else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
297 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
298 switch (keyEvent->key()) {
299 case Qt::Key_Left:
300 case Qt::Key_Right:
301 case Qt::Key_Up:
302 case Qt::Key_Down:
303 case Qt::Key_PageUp:
304 case Qt::Key_PageDown:
305 MessageView::event(event);
306 return true;
307 case Qt::Key_Home:
308 case Qt::Key_End:
309 return false;
310 default:
311 return QObject::eventFilter(object, event);
313 } else {
314 return QObject::eventFilter(object, event);
318 QString MessageView::quoteText() const
320 if (const AbstractPartWidget *w = dynamic_cast<const AbstractPartWidget *>(viewer)) {
321 QStringList quote = Composer::quoteText(w->quoteMe().split(QLatin1Char('\n')));
322 const Imap::Message::Envelope &e = message.data(Imap::Mailbox::RoleMessageEnvelope).value<Imap::Message::Envelope>();
323 QString sender;
324 if (!e.from.isEmpty())
325 sender = e.from[0].prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME);
326 if (e.from.isEmpty())
327 sender = tr("you");
329 // One extra newline at the end of the quoted text to separate the response
330 quote << QString();
332 return tr("On %1, %2 wrote:\n").arg(e.date.toLocalTime().toString(Qt::SystemLocaleLongDate)).arg(sender) + quote.join(QLatin1String("\n"));
334 return QString();
337 void MessageView::setNetworkWatcher(Imap::Mailbox::NetworkWatcher *netWatcher)
339 m_netWatcher = netWatcher;
340 factory->setNetworkWatcher(netWatcher);
343 void MessageView::reply(MainWindow *mainWindow, Composer::ReplyMode mode)
345 if (!message.isValid())
346 return;
348 // The Message-Id of the original message might have been empty; be sure we can handle that
349 QByteArray messageId = message.data(Imap::Mailbox::RoleMessageMessageId).toByteArray();
350 QList<QByteArray> messageIdList;
351 if (!messageId.isEmpty()) {
352 messageIdList.append(messageId);
355 ComposeWidget::warnIfMsaNotConfigured(
356 ComposeWidget::createReply(mainWindow, mode, message, QList<QPair<Composer::RecipientKind,QString> >(),
357 Composer::Util::replySubject(message.data(Imap::Mailbox::RoleMessageSubject).toString()),
358 quoteText(), messageIdList,
359 message.data(Imap::Mailbox::RoleMessageHeaderReferences).value<QList<QByteArray> >() + messageIdList),
360 mainWindow);
363 void MessageView::forward(MainWindow *mainWindow, const Composer::ForwardMode mode)
365 if (!message.isValid())
366 return;
368 // The Message-Id of the original message might have been empty; be sure we can handle that
369 QByteArray messageId = message.data(Imap::Mailbox::RoleMessageMessageId).toByteArray();
370 QList<QByteArray> messageIdList;
371 if (!messageId.isEmpty()) {
372 messageIdList.append(messageId);
375 ComposeWidget::warnIfMsaNotConfigured(
376 ComposeWidget::createForward(mainWindow, mode, message, Composer::Util::forwardSubject(message.data(Imap::Mailbox::RoleMessageSubject).toString()),
377 messageIdList, message.data(Imap::Mailbox::RoleMessageHeaderReferences).value<QList<QByteArray>>() + messageIdList),
378 mainWindow);
381 void MessageView::externalsRequested(const QUrl &url)
383 Q_UNUSED(url);
384 externalElements->show();
387 void MessageView::externalsEnabled()
389 netAccess->setExternalsEnabled(true);
390 externalElements->hide();
391 AbstractPartWidget *w = dynamic_cast<AbstractPartWidget *>(viewer);
392 if (w)
393 w->reloadContents();
396 void MessageView::newLabelAction(const QString &tag)
398 if (!message.isValid())
399 return;
401 Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
402 model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_ADD);
405 void MessageView::deleteLabelAction(const QString &tag)
407 if (!message.isValid())
408 return;
410 Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
411 model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_REMOVE);
414 void MessageView::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
416 Q_ASSERT(topLeft.row() == bottomRight.row() && topLeft.parent() == bottomRight.parent());
417 if (topLeft == message) {
418 if (viewer == emptyView && message.data(Imap::Mailbox::RoleIsFetched).toBool()) {
419 qDebug() << "MessageView: message which was previously not loaded has just became available";
420 setEmpty();
421 setMessage(topLeft);
423 tags->setTagList(message.data(Imap::Mailbox::RoleMessageFlags).toStringList());
427 void MessageView::setHomepageUrl(const QUrl &homepage)
429 emptyView->load(homepage);
432 void MessageView::showEvent(QShowEvent *se)
434 QWidget::showEvent(se);
435 // The Oxygen style reset the attribute - since we're gonna cause an update() here anyway, it's
436 // a good moment to stress that "we know better, Hugo ;-)" -- Thomas
437 setAutoFillBackground(true);
440 void MessageView::headerLinkActivated(QString s)
442 // Trojita is registered to handle any mailto: URL
443 QDesktopServices::openUrl(QUrl(s));
446 void MessageView::partContextMenuRequested(const QPoint &point)
448 if (SimplePartWidget *w = qobject_cast<SimplePartWidget *>(sender())) {
449 QMenu menu(w);
450 Q_FOREACH(QAction *action, w->contextMenuSpecificActions())
451 menu.addAction(action);
452 menu.addAction(w->pageAction(QWebPage::Copy));
453 menu.addAction(w->pageAction(QWebPage::SelectAll));
454 if (!w->page()->mainFrame()->hitTestContent(point).linkUrl().isEmpty()) {
455 menu.addSeparator();
456 menu.addAction(w->pageAction(QWebPage::CopyLinkToClipboard));
458 menu.exec(w->mapToGlobal(point));
462 void MessageView::partLinkHovered(const QString &link, const QString &title, const QString &textContent)
464 Q_UNUSED(title);
465 Q_UNUSED(textContent);
466 emit linkHovered(link);
469 void MessageView::triggerSearchDialog()
471 emit searchRequestedBy(qobject_cast<EmbeddedWebView*>(sender()));
474 QModelIndex MessageView::currentMessage() const
476 return message;
479 void MessageView::onWebViewLoadStarted()
481 QWebView *wv = qobject_cast<QWebView*>(sender());
482 Q_ASSERT(wv);
484 if (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE) {
485 m_loadingItems << wv;
486 m_loadingSpinner->start(250);
490 void MessageView::onWebViewLoadFinished()
492 QWebView *wv = qobject_cast<QWebView*>(sender());
493 Q_ASSERT(wv);
494 m_loadingItems.remove(wv);
495 if (m_loadingItems.isEmpty())
496 m_loadingSpinner->stop();