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/>.
23 #include <QDesktopServices>
24 #include <QHeaderView>
28 #include <QTextDocument>
31 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
34 #include <QVBoxLayout>
36 #include <QWebHistory>
37 #include <QWebHitTestResult>
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"
51 #include "Imap/Model/MailboxTree.h"
52 #include "Imap/Model/MsgListModel.h"
53 #include "Imap/Network/MsgPartNetAccessManager.h"
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
));
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);
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
);
99 header
->setWordWrap(true);
100 connect(header
, SIGNAL(linkHovered(QString
)), this, SLOT(linkInTitleHovered(QString
)));
101 connect(header
, SIGNAL(linkActivated(QString
)), this, SLOT(headerLinkActivated(QString
)));
104 tags
= new TagListWidget(headerSection
);
105 tags
->setBackgroundRole(helpingHeader
.backgroundRole());
106 tags
->setForegroundRole(helpingHeader
.foregroundRole());
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()));
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
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
) {
168 void MessageView::setEmpty()
170 markAsReadTimer
->stop();
171 header
->setText(QString());
172 headerSection
->hide();
173 message
= QModelIndex();
174 disconnect(this, SLOT(handleDataChanged(QModelIndex
,QModelIndex
)));
176 if (viewer
!= emptyView
) {
177 layout
->removeWidget(viewer
);
178 viewer
->deleteLater();
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
);
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.
205 connect(realModel
, SIGNAL(dataChanged(QModelIndex
,QModelIndex
)), this, SLOT(handleDataChanged(QModelIndex
,QModelIndex
)));
206 message
= messageIndex
;
210 QModelIndex rootPartIndex
= messageIndex
.child(0, 0);
212 headerSection
->show();
213 if (message
!= messageIndex
) {
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
);
230 header
->setText(headerText());
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())
251 Imap::Mailbox::Model
*model
= const_cast<Imap::Mailbox::Model
*>(dynamic_cast<const Imap::Mailbox::Model
*>(message
.model()));
253 if (!model
->isNetworkAvailable())
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
);
267 parentWidget()->setFocusPolicy(Qt::StrongFocus
);
269 } else if (event
->type() == QEvent::KeyPress
|| event
->type() == QEvent::KeyRelease
) {
270 QKeyEvent
*keyEvent
= static_cast<QKeyEvent
*>(event
);
271 switch (keyEvent
->key()) {
277 case Qt::Key_PageDown
:
280 MessageView::event(event
);
283 return QObject::eventFilter(object
, event
);
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())
303 const Imap::Message::Envelope
&e
= envelope();
306 if (!e
.from
.isEmpty())
307 res
+= tr("<b>From:</b> %1<br/>").arg(Imap::Message::MailAddress::prettyList(e
.from
, Imap::Message::MailAddress::FORMAT_CLICKABLE
));
309 res
+= tr("<b>To:</b> %1<br/>").arg(Imap::Message::MailAddress::prettyList(e
.to
, Imap::Message::MailAddress::FORMAT_CLICKABLE
));
311 res
+= tr("<b>Cc:</b> %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> %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> %1").arg(Qt::escape(e
.subject
));
317 res
+= tr("<b>Subject:</b> %1").arg(e
.subject
.toHtmlEscaped());
319 if (e
.date
.isValid())
320 res
+= tr("<br/><b>Date:</b> %1").arg(e
.date
.toLocalTime().toString(Qt::SystemLocaleLongDate
));
324 QString
MessageView::quoteText() const
326 if (const AbstractPartWidget
*w
= dynamic_cast<const AbstractPartWidget
*>(viewer
)) {
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
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) == '>')
344 // long line -> needs to be wrapped
345 // 1st, detect the quote depth and eventually stript the quotes from the line
347 int contentStart
= 0;
348 if (line
->at(0) == '>') {
350 while (quoteLevel
< line
->length() && line
->at(quoteLevel
) == '>')
352 contentStart
= quoteLevel
;
353 if (quoteLevel
< line
->length() && line
->at(quoteLevel
) == ' ')
357 // 2nd, build a qute string
359 for (int i
= 0; i
< quoteLevel
; ++i
)
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
) == ' ')
369 if (length
> 65-quotemarks
.length() && space
!= lastSpace
) {
371 quote
<< quotemarks
+ line
->mid(lastSpace
, space
- lastSpace
);
373 length
= pos
- space
;
377 quote
<< quotemarks
+ line
->mid(lastSpace
);
379 const Imap::Message::Envelope
&e
= envelope();
381 if (!e
.from
.isEmpty())
382 sender
= e
.from
[0].prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME
);
383 if (e
.from
.isEmpty())
386 // One extra newline at the end of the quoted text to separate the response
389 return tr("On %1, %2 wrote:\n").arg(e
.date
.toLocalTime().toString(Qt::SystemLocaleLongDate
)).arg(sender
) + quote
.join("\n");
394 void MessageView::reply(MainWindow
*mainWindow
, ReplyMode mode
)
396 if (!message
.isValid())
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
,
420 void MessageView::externalsRequested(const QUrl
&url
)
423 externalElements
->show();
426 void MessageView::externalsEnabled()
428 netAccess
->setExternalsEnabled(true);
429 externalElements
->hide();
430 AbstractPartWidget
*w
= dynamic_cast<AbstractPartWidget
*>(viewer
);
435 void MessageView::linkInTitleHovered(const QString
&target
)
437 if (target
.isEmpty()) {
438 header
->setToolTip(QString());
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
)));
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());
462 void MessageView::newLabelAction(const QString
&tag
)
464 if (!message
.isValid())
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())
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";
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())) {
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()) {
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
)
530 Q_UNUSED(textContent
);
531 emit
linkHovered(link
);