Mark the messages as \Answered when the reply was sent
[trojita.git] / src / Gui / MessageView.cpp
blob64f09a5419295fd33ee58e75d4c64a9e908ffea3
1 /* Copyright (C) 2006 - 2012 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include <QDebug>
23 #include <QDesktopServices>
24 #include <QHeaderView>
25 #include <QKeyEvent>
26 #include <QLabel>
27 #include <QMenu>
28 #include <QTextDocument>
29 #include <QTimer>
30 #include <QUrl>
31 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
32 #include <QUrlQuery>
33 #endif
34 #include <QVBoxLayout>
35 #include <QWebFrame>
36 #include <QWebHistory>
37 #include <QWebHitTestResult>
38 #include <QWebPage>
40 #include "MessageView.h"
41 #include "AbstractPartWidget.h"
42 #include "Composer/SubjectMangling.h"
43 #include "EmbeddedWebView.h"
44 #include "ExternalElementsWidget.h"
45 #include "PartWidgetFactory.h"
46 #include "SimplePartWidget.h"
47 #include "TagListWidget.h"
48 #include "UserAgentWebPage.h"
49 #include "Window.h"
51 #include "Imap/Model/MailboxTree.h"
52 #include "Imap/Model/MsgListModel.h"
53 #include "Imap/Network/MsgPartNetAccessManager.h"
55 namespace Gui
58 MessageView::MessageView(QWidget *parent): QWidget(parent)
60 QPalette pal = palette();
61 pal.setColor(backgroundRole(), palette().color(QPalette::Active, QPalette::Base));
62 pal.setColor(foregroundRole(), palette().color(QPalette::Active, QPalette::Text));
63 setPalette(pal);
64 setAutoFillBackground(true);
65 setFocusPolicy(Qt::StrongFocus); // not by the wheel
66 netAccess = new Imap::Network::MsgPartNetAccessManager(this);
67 connect(netAccess, SIGNAL(requestingExternal(QUrl)), this, SLOT(externalsRequested(QUrl)));
68 factory = new PartWidgetFactory(netAccess, this, this);
70 emptyView = new EmbeddedWebView(this, new QNetworkAccessManager(this));
71 emptyView->setFixedSize(450,300);
72 QMetaObject::invokeMethod(emptyView, "handlePageLoadFinished", Qt::QueuedConnection, Q_ARG(bool, true));
73 emptyView->setPage(new UserAgentWebPage(emptyView));
74 emptyView->installEventFilter(this);
75 emptyView->setAutoFillBackground(false);
77 viewer = emptyView;
79 //BEGIN create header section
81 headerSection = new QWidget(this);
83 // we create a dummy header, pass it through the style and the use it's color roles so we
84 // know what headers in general look like in the system
85 QHeaderView helpingHeader(Qt::Horizontal);
86 helpingHeader.ensurePolished();
87 pal = headerSection->palette();
88 pal.setColor(headerSection->backgroundRole(), palette().color(QPalette::Active, helpingHeader.backgroundRole()));
89 pal.setColor(headerSection->foregroundRole(), palette().color(QPalette::Active, helpingHeader.foregroundRole()));
90 headerSection->setPalette(pal);
91 headerSection->setAutoFillBackground(true);
93 // the actual mail header
94 header = new QLabel(headerSection);
95 header->setBackgroundRole(helpingHeader.backgroundRole());
96 header->setForegroundRole(helpingHeader.foregroundRole());
97 header->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
98 header->setIndent(5);
99 header->setWordWrap(true);
100 connect(header, SIGNAL(linkHovered(QString)), this, SLOT(linkInTitleHovered(QString)));
101 connect(header, SIGNAL(linkActivated(QString)), this, SLOT(headerLinkActivated(QString)));
103 // the tag bar
104 tags = new TagListWidget(headerSection);
105 tags->setBackgroundRole(helpingHeader.backgroundRole());
106 tags->setForegroundRole(helpingHeader.foregroundRole());
107 tags->hide();
108 connect(tags, SIGNAL(tagAdded(QString)), this, SLOT(newLabelAction(QString)));
109 connect(tags, SIGNAL(tagRemoved(QString)), this, SLOT(deleteLabelAction(QString)));
111 // whether we allow to load external elements
112 externalElements = new ExternalElementsWidget(this);
113 externalElements->hide();
114 connect(externalElements, SIGNAL(loadingEnabled()), this, SLOT(externalsEnabled()));
116 // layout the header
117 layout = new QVBoxLayout(headerSection);
118 layout->addWidget(header, 1);
119 layout->addWidget(tags, 3);
120 layout->addWidget(externalElements, 1);
122 //END create header section
124 //BEGIN layout the message
126 layout = new QVBoxLayout(this);
127 layout->setSpacing(0);
128 layout->setContentsMargins(0,0,0,0);
130 layout->addWidget(headerSection, 1);
132 headerSection->hide();
134 // put the actual messages into an extra horizontal view
135 // this allows us easy usage of the trailing stretch and also to indent the message a bit
136 QHBoxLayout *hLayout = new QHBoxLayout;
137 hLayout->setContentsMargins(6,6,6,0);
138 hLayout->addWidget(viewer);
139 static_cast<QVBoxLayout*>(layout)->addLayout(hLayout, 1);
140 // add a strong stretch to squeeze header and message to the top
141 // possibly passing a large stretch factor to the message could be enough...
142 layout->addStretch(1000);
144 //END layout the message
146 // make the layout used to add messages our new horizontal layout
147 layout = hLayout;
149 markAsReadTimer = new QTimer(this);
150 markAsReadTimer->setSingleShot(true);
151 connect(markAsReadTimer, SIGNAL(timeout()), this, SLOT(markAsRead()));
154 MessageView::~MessageView()
156 // Redmine #496 -- the default order of destruction starts with our QNAM subclass which in turn takes care of all pending
157 // QNetworkReply instances created by that manager. When the destruction goes to the WebKit objects, they try to disconnect
158 // from the network replies which are however gone already. We can mitigate that by simply making sure that the destruction
159 // starts with the QWebView subclasses and only after that proceeds to the QNAM. Qt's default order leads to segfaults here.
160 if (viewer != emptyView) {
161 delete viewer;
163 delete emptyView;
165 delete factory;
168 void MessageView::setEmpty()
170 markAsReadTimer->stop();
171 header->setText(QString());
172 headerSection->hide();
173 message = QModelIndex();
174 disconnect(this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
175 tags->hide();
176 if (viewer != emptyView) {
177 layout->removeWidget(viewer);
178 viewer->deleteLater();
179 viewer = emptyView;
180 viewer->show();
181 layout->addWidget(viewer);
182 emit messageChanged();
186 void MessageView::setMessage(const QModelIndex &index)
188 // first, let's get a real model
189 QModelIndex messageIndex;
190 const Imap::Mailbox::Model *constModel = 0;
191 Imap::Mailbox::TreeItem *item = Imap::Mailbox::Model::realTreeItem(index, &constModel, &messageIndex);
192 Q_ASSERT(item); // Make sure it's a message
193 Q_ASSERT(messageIndex.isValid());
194 Imap::Mailbox::Model *realModel = const_cast<Imap::Mailbox::Model *>(constModel);
195 Q_ASSERT(realModel);
197 // The data might be available from the local cache, so let's try to save a possible roundtrip here
198 item->fetch(realModel);
200 if (!messageIndex.data(Imap::Mailbox::RoleIsFetched).toBool()) {
201 // This happens when the message placeholder is already available in the GUI, but the actual message data haven't been
202 // loaded yet. This is especially common with the threading model.
203 // Note that the data might be already available in the cache, it's just that it isn't in the mailbox tree yet.
204 setEmpty();
205 connect(realModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
206 message = messageIndex;
207 return;
210 QModelIndex rootPartIndex = messageIndex.child(0, 0);
212 headerSection->show();
213 if (message != messageIndex) {
214 emptyView->hide();
215 layout->removeWidget(viewer);
216 if (viewer != emptyView) {
217 viewer->setParent(0);
218 viewer->deleteLater();
220 message = messageIndex;
221 netAccess->setExternalsEnabled(false);
222 externalElements->hide();
224 netAccess->setModelMessage(message);
226 viewer = factory->create(rootPartIndex);
227 viewer->setParent(this);
228 layout->addWidget(viewer);
229 viewer->show();
230 header->setText(headerText());
232 tags->show();
233 tags->setTagList(messageIndex.data(Imap::Mailbox::RoleMessageFlags).toStringList());
234 disconnect(this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
235 connect(realModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(handleDataChanged(QModelIndex,QModelIndex)));
237 emit messageChanged();
239 // We want to propagate the QWheelEvent to upper layers
240 viewer->installEventFilter(this);
243 if (realModel->isNetworkAvailable())
244 markAsReadTimer->start(200); // FIXME: make this configurable
247 void MessageView::markAsRead()
249 if (!message.isValid())
250 return;
251 Imap::Mailbox::Model *model = const_cast<Imap::Mailbox::Model *>(dynamic_cast<const Imap::Mailbox::Model *>(message.model()));
252 Q_ASSERT(model);
253 if (!model->isNetworkAvailable())
254 return;
255 if (!message.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool())
256 model->markMessagesRead(QModelIndexList() << message, Imap::Mailbox::FLAG_ADD);
259 bool MessageView::eventFilter(QObject *object, QEvent *event)
261 if (event->type() == QEvent::Wheel) {
262 // while the containing scrollview has Qt::StrongFocus, the event forwarding breaks that
263 // -> completely disable focus for the following wheel event ...
264 parentWidget()->setFocusPolicy(Qt::NoFocus);
265 MessageView::event(event);
266 // ... set reset it
267 parentWidget()->setFocusPolicy(Qt::StrongFocus);
268 return true;
269 } else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
270 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
271 switch (keyEvent->key()) {
272 case Qt::Key_Left:
273 case Qt::Key_Right:
274 case Qt::Key_Up:
275 case Qt::Key_Down:
276 case Qt::Key_PageUp:
277 case Qt::Key_PageDown:
278 case Qt::Key_Home:
279 case Qt::Key_End:
280 MessageView::event(event);
281 return true;
282 default:
283 return QObject::eventFilter(object, event);
285 } else {
286 return QObject::eventFilter(object, event);
290 Imap::Message::Envelope MessageView::envelope() const
292 // Accessing the envelope via QVariant is just too much work here; it's way easier to just get the raw pointer
293 Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
294 Imap::Mailbox::TreeItemMessage *messagePtr = dynamic_cast<Imap::Mailbox::TreeItemMessage *>(static_cast<Imap::Mailbox::TreeItem *>(message.internalPointer()));
295 return messagePtr->envelope(model);
298 QString MessageView::headerText()
300 if (!message.isValid())
301 return QString();
303 const Imap::Message::Envelope &e = envelope();
305 QString res;
306 if (!e.from.isEmpty())
307 res += tr("<b>From:</b>&nbsp;%1<br/>").arg(Imap::Message::MailAddress::prettyList(e.from, Imap::Message::MailAddress::FORMAT_CLICKABLE));
308 if (!e.to.isEmpty())
309 res += tr("<b>To:</b>&nbsp;%1<br/>").arg(Imap::Message::MailAddress::prettyList(e.to, Imap::Message::MailAddress::FORMAT_CLICKABLE));
310 if (!e.cc.isEmpty())
311 res += tr("<b>Cc:</b>&nbsp;%1<br/>").arg(Imap::Message::MailAddress::prettyList(e.cc, Imap::Message::MailAddress::FORMAT_CLICKABLE));
312 if (!e.bcc.isEmpty())
313 res += tr("<b>Bcc:</b>&nbsp;%1<br/>").arg(Imap::Message::MailAddress::prettyList(e.bcc, Imap::Message::MailAddress::FORMAT_CLICKABLE));
314 #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
315 res += tr("<b>Subject:</b>&nbsp;%1").arg(Qt::escape(e.subject));
316 #else
317 res += tr("<b>Subject:</b>&nbsp;%1").arg(e.subject.toHtmlEscaped());
318 #endif
319 if (e.date.isValid())
320 res += tr("<br/><b>Date:</b>&nbsp;%1").arg(e.date.toLocalTime().toString(Qt::SystemLocaleLongDate));
321 return res;
324 QString MessageView::quoteText() const
326 if (const AbstractPartWidget *w = dynamic_cast<const AbstractPartWidget *>(viewer)) {
327 QStringList quote;
328 QStringList lines = w->quoteMe().split('\n');
329 for (QStringList::iterator line = lines.begin(); line != lines.end(); ++line) {
330 if (*line == QLatin1String("-- ")) {
331 // This is the signature separator, we should not include anything below that in the quote
332 break;
334 // rewrap - we need to keep the quotes at < 79 chars, yet the grow with every level
335 if (line->length() < 79-2) {
336 // this line is short enough, prepend quote mark and continue
337 if (line->isEmpty() || line->at(0) == '>')
338 line->prepend(">");
339 else
340 line->prepend("> ");
341 quote << *line;
342 continue;
344 // long line -> needs to be wrapped
345 // 1st, detect the quote depth and eventually stript the quotes from the line
346 int quoteLevel = 0;
347 int contentStart = 0;
348 if (line->at(0) == '>') {
349 quoteLevel = 1;
350 while (quoteLevel < line->length() && line->at(quoteLevel) == '>')
351 ++quoteLevel;
352 contentStart = quoteLevel;
353 if (quoteLevel < line->length() && line->at(quoteLevel) == ' ')
354 ++contentStart;
357 // 2nd, build a qute string
358 QString quotemarks;
359 for (int i = 0; i < quoteLevel; ++i)
360 quotemarks += ">";
361 quotemarks += "> ";
363 // 3rd, wrap the line, prepend the quotemarks to each line and add it to the quote text
364 int space(contentStart), lastSpace(contentStart), pos(contentStart), length(0);
365 while (pos < line->length()) {
366 if (line->at(pos) == ' ')
367 space = pos+1;
368 ++length;
369 if (length > 65-quotemarks.length() && space != lastSpace) {
370 // wrap
371 quote << quotemarks + line->mid(lastSpace, space - lastSpace);
372 lastSpace = space;
373 length = pos - space;
375 ++pos;
377 quote << quotemarks + line->mid(lastSpace);
379 const Imap::Message::Envelope &e = envelope();
380 QString sender;
381 if (!e.from.isEmpty())
382 sender = e.from[0].prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME);
383 if (e.from.isEmpty())
384 sender = tr("you");
386 // One extra newline at the end of the quoted text to separate the response
387 quote << QString();
389 return tr("On %1, %2 wrote:\n").arg(e.date.toLocalTime().toString(Qt::SystemLocaleLongDate)).arg(sender) + quote.join("\n");
391 return QString();
394 void MessageView::reply(MainWindow *mainWindow, ReplyMode mode)
396 if (!message.isValid())
397 return;
399 const Imap::Message::Envelope &e = envelope();
401 QList<QPair<Imap::Mailbox::MessageComposer::RecipientKind,QString> > recipients;
402 for (QList<Imap::Message::MailAddress>::const_iterator it = e.from.begin(); it != e.from.end(); ++it) {
403 recipients << qMakePair(Imap::Mailbox::MessageComposer::Recipient_To, QString::fromUtf8("%1@%2").arg(it->mailbox, it->host));
405 if (mode == REPLY_ALL) {
406 for (QList<Imap::Message::MailAddress>::const_iterator it = e.to.begin(); it != e.to.end(); ++it) {
407 recipients << qMakePair(Imap::Mailbox::MessageComposer::Recipient_Cc, QString::fromUtf8("%1@%2").arg(it->mailbox, it->host));
409 for (QList<Imap::Message::MailAddress>::const_iterator it = e.cc.begin(); it != e.cc.end(); ++it) {
410 recipients << qMakePair(Imap::Mailbox::MessageComposer::Recipient_To, QString::fromUtf8("%1@%2").arg(it->mailbox, it->host));
413 mainWindow->invokeComposeDialog(Composer::Util::replySubject(e.subject), quoteText(), recipients,
414 QList<QByteArray>() << e.messageId,
415 message.data(Imap::Mailbox::RoleMessageHeaderReferences).value<QList<QByteArray> >() << e.messageId,
416 message
420 void MessageView::externalsRequested(const QUrl &url)
422 Q_UNUSED(url);
423 externalElements->show();
426 void MessageView::externalsEnabled()
428 netAccess->setExternalsEnabled(true);
429 externalElements->hide();
430 AbstractPartWidget *w = dynamic_cast<AbstractPartWidget *>(viewer);
431 if (w)
432 w->reloadContents();
435 void MessageView::linkInTitleHovered(const QString &target)
437 if (target.isEmpty()) {
438 header->setToolTip(QString());
439 return;
442 QUrl url(target);
444 QString frontOfAtSign, afterAtSign;
445 if (url.path().indexOf(QLatin1String("@")) != -1) {
446 QStringList chunks = url.path().split(QLatin1String("@"));
447 frontOfAtSign = chunks[0];
448 afterAtSign = QStringList(chunks.mid(1)).join(QLatin1String("@"));
450 #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
451 Imap::Message::MailAddress addr(url.queryItemValue(QLatin1String("X-Trojita-DisplayName")), QString(),
452 frontOfAtSign, afterAtSign);
453 header->setToolTip(Qt::escape(addr.prettyName(Imap::Message::MailAddress::FORMAT_READABLE)));
454 #else
455 QUrlQuery q(url);
456 Imap::Message::MailAddress addr(q.queryItemValue(QLatin1String("X-Trojita-DisplayName")), QString(),
457 frontOfAtSign, afterAtSign);
458 header->setToolTip(addr.prettyName(Imap::Message::MailAddress::FORMAT_READABLE).toHtmlEscaped());
459 #endif
462 void MessageView::newLabelAction(const QString &tag)
464 if (!message.isValid())
465 return;
467 Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
468 model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_ADD);
471 void MessageView::deleteLabelAction(const QString &tag)
473 if (!message.isValid())
474 return;
476 Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
477 model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_REMOVE);
480 void MessageView::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
482 Q_ASSERT(topLeft.row() == bottomRight.row() && topLeft.parent() == bottomRight.parent());
483 if (topLeft == message) {
484 if (viewer == emptyView && message.data(Imap::Mailbox::RoleIsFetched).toBool()) {
485 qDebug() << "MessageView: message which was previously not loaded has just became available";
486 setEmpty();
487 setMessage(topLeft);
489 tags->setTagList(message.data(Imap::Mailbox::RoleMessageFlags).toStringList());
493 void MessageView::setHomepageUrl(const QUrl &homepage)
495 emptyView->load(homepage);
498 void MessageView::showEvent(QShowEvent *se)
500 QWidget::showEvent(se);
501 // The Oxygen style reset the attribute - since we're gonna cause an update() here anyway, it's
502 // a good moment to stress that "we know better, Hugo ;-)" -- Thomas
503 setAutoFillBackground(true);
506 void MessageView::headerLinkActivated(QString s)
508 // Trojita is registered to handle any mailto: URL
509 QDesktopServices::openUrl(QUrl(s));
512 void MessageView::partContextMenuRequested(const QPoint &point)
514 if (SimplePartWidget *w = qobject_cast<SimplePartWidget *>(sender())) {
515 QMenu menu(w);
516 Q_FOREACH(QAction *action, w->contextMenuSpecificActions())
517 menu.addAction(action);
518 menu.addAction(w->pageAction(QWebPage::SelectAll));
519 if (!w->page()->mainFrame()->hitTestContent(point).linkUrl().isEmpty()) {
520 menu.addSeparator();
521 menu.addAction(w->pageAction(QWebPage::CopyLinkToClipboard));
523 menu.exec(w->mapToGlobal(point));
527 void MessageView::partLinkHovered(const QString &link, const QString &title, const QString &textContent)
529 Q_UNUSED(title);
530 Q_UNUSED(textContent);
531 emit linkHovered(link);