1 /* Copyright (C) 2006 - 2014 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 "AttachmentView.h"
24 #include <QApplication>
25 #include <QDesktopServices>
27 #include <QFileDialog>
28 #include <QHBoxLayout>
31 #include <QMimeDatabase>
32 #include <QMouseEvent>
34 #include <QPushButton>
37 #include <QStyleOption>
38 #include <QTemporaryFile>
40 #include <QToolButton>
42 #include "Common/Paths.h"
43 #include "Gui/MessageView.h" // so that the compiler knows it's a QObject
44 #include "Gui/Window.h"
45 #include "Imap/Network/FileDownloadManager.h"
46 #include "Imap/Model/DragAndDrop.h"
47 #include "Imap/Model/MailboxTree.h"
48 #include "Imap/Model/ItemRoles.h"
49 #include "UiUtils/Formatting.h"
50 #include "UiUtils/IconLoader.h"
55 AttachmentView::AttachmentView(QWidget
*parent
, Imap::Network::MsgPartNetAccessManager
*manager
,
56 const QModelIndex
&partIndex
, MessageView
*messageView
, QWidget
*contentWidget
)
58 , m_partIndex(partIndex
)
59 , m_messageView(messageView
)
60 , m_downloadAttachment(nullptr)
61 , m_openAttachment(nullptr)
62 , m_showHideAttachment(nullptr)
63 , m_showSource(nullptr)
64 , m_netAccess(manager
)
66 , m_contentWidget(contentWidget
)
68 setSizePolicy(QSizePolicy::Preferred
, QSizePolicy::Fixed
);
69 setFrameStyle(QFrame::NoFrame
);
70 setCursor(Qt::OpenHandCursor
);
71 setAttribute(Qt::WA_Hover
);
73 // not actually required, but styles may assume the parameter and segfault on nullptr deref
77 const int padding
= style()->pixelMetric(QStyle::PM_DefaultFrameWidth
, &opt
, this);
78 setContentsMargins(padding
, 0, padding
, 0);
80 QHBoxLayout
*layout
= new QHBoxLayout();
81 layout
->setContentsMargins(0,0,0,0);
83 // should be PM_LayoutHorizontalSpacing, but is not implemented by many Qt4 styles -including oxygen- for other conflicts
84 int spacing
= style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing
, &opt
, this);
86 spacing
= style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing
, &opt
, this);
87 layout
->setSpacing(0);
89 m_menu
= new QMenu(this);
90 m_downloadAttachment
= m_menu
->addAction(UiUtils::loadIcon(QStringLiteral("document-save-as")), tr("Download"));
91 m_openAttachment
= m_menu
->addAction(tr("Open Directly"));
92 connect(m_downloadAttachment
, &QAction::triggered
, this, &AttachmentView::slotDownloadAttachment
);
93 connect(m_openAttachment
, &QAction::triggered
, this, &AttachmentView::slotOpenAttachment
);
94 connect(m_menu
, &QMenu::aboutToShow
, this, &AttachmentView::updateShowHideAttachmentState
);
95 if (m_contentWidget
) {
96 m_showHideAttachment
= m_menu
->addAction(UiUtils::loadIcon(QStringLiteral("view-preview")), tr("Show Preview"));
97 m_showHideAttachment
->setCheckable(true);
98 m_showHideAttachment
->setChecked(!m_contentWidget
->isHidden());
99 connect(m_showHideAttachment
, &QAction::triggered
, m_contentWidget
, &QWidget::setVisible
);
100 connect(m_showHideAttachment
, &QAction::triggered
, this, &AttachmentView::updateShowHideAttachmentState
);
102 if (partIndex
.data(Imap::Mailbox::RolePartMimeType
).toByteArray() == "message/rfc822") {
103 m_showSource
= m_menu
->addAction(UiUtils::loadIcon(QStringLiteral("text-x-hex")), tr("Show Message Source"));
104 connect(m_showSource
, &QAction::triggered
, this, &AttachmentView::showMessageSource
);
108 m_icon
= new QToolButton(this);
109 m_icon
->setAttribute(Qt::WA_NoMousePropagation
, false); // inform us for DnD
110 m_icon
->setAutoRaise(true);
111 m_icon
->setIconSize(QSize(22,22));
112 m_icon
->setToolButtonStyle(Qt::ToolButtonIconOnly
);
113 m_icon
->setPopupMode(QToolButton::MenuButtonPopup
);
114 m_icon
->setMenu(m_menu
);
115 connect(m_icon
, &QAbstractButton::pressed
, this, &AttachmentView::toggleIconCursor
);
116 connect(m_icon
, &QAbstractButton::clicked
, this, &AttachmentView::showMenuOrPreview
);
117 connect(m_icon
, &QAbstractButton::released
, this, &AttachmentView::toggleIconCursor
);
118 m_icon
->setCursor(Qt::ArrowCursor
);
120 QString mimeDescription
= partIndex
.data(Imap::Mailbox::RolePartMimeType
).toString();
121 QString rawMime
= mimeDescription
;
122 QMimeType mimeType
= QMimeDatabase().mimeTypeForName(mimeDescription
);
123 if (rawMime
== QStringLiteral("application/x-trojita-malformed-part-from-imap-response")) {
124 mimeDescription
= QString::fromUtf8(partIndex
.data(Imap::Mailbox::RolePartBodyFldParam
)
125 .value
<Imap::Message::AbstractMessage::bodyFldParam_t
>()
126 .value("x-trojita-original-mime-type"));
127 mimeDescription
= tr("IMAP Server error for this part: %1 (%2)").arg(
128 QMimeDatabase().mimeTypeForName(mimeDescription
).comment(), mimeDescription
);
129 m_icon
->setIcon(UiUtils::loadIcon(QStringLiteral("emblem-warning")));
130 } else if (mimeType
.isValid() && !mimeType
.isDefault()) {
131 mimeDescription
= mimeType
.comment();
133 if (rawMime
== QLatin1String("message/rfc822")) {
134 // Special case for plain e-mail messages. Motivation for this is that most of the OSes ship these icons
135 // with a pixmap which shows something like a sheet of paper as the background. I find it rather dumb
136 // to do this in the context of a MUA where attached messages are pretty common, which is why this special
137 // case is in place. Comments welcome.
138 icon
= UiUtils::loadIcon(QStringLiteral("trojita"));
140 icon
= QIcon::fromTheme(mimeType
.iconName(),
141 QIcon::fromTheme(mimeType
.genericIconName(), UiUtils::loadIcon(QStringLiteral("mail-attachment")))
144 m_icon
->setIcon(icon
);
146 m_icon
->setIcon(UiUtils::loadIcon(QStringLiteral("mail-attachment")));
149 layout
->addWidget(m_icon
);
151 // space between icon and label
152 layout
->addSpacing(spacing
);
154 QVBoxLayout
*subLayout
= new QVBoxLayout
;
155 subLayout
->setContentsMargins(0,0,0,0);
156 // The file name shall be mouse-selectable
157 m_fileName
= new QLabel(this);
158 m_fileName
->setTextFormat(Qt::PlainText
);
159 m_fileName
->setText(partIndex
.data(Imap::Mailbox::RolePartFileName
).toString());
160 m_fileName
->setTextInteractionFlags(Qt::TextSelectableByMouse
);
161 m_fileName
->setCursor(Qt::IBeamCursor
);
162 m_fileName
->setSizePolicy(QSizePolicy::Fixed
, QSizePolicy::Fixed
);
163 subLayout
->addWidget(m_fileName
);
165 // Some metainformation -- the MIME type and the file size
166 QLabel
*lbl
= new QLabel(tr("%2, %3").arg(mimeDescription
,
167 UiUtils::Formatting::prettySize(partIndex
.data(Imap::Mailbox::RolePartOctets
).toULongLong())));
168 if (rawMime
!= mimeDescription
) {
169 lbl
->setToolTip(rawMime
);
171 QFont
f(lbl
->font());
173 if (f
.pointSize() > -1) // don't try the below on pixel fonts ever.
174 f
.setPointSizeF(f
.pointSizeF() * 0.8);
176 subLayout
->addWidget(lbl
);
177 layout
->addLayout(subLayout
);
179 // space between label and arrow
180 layout
->addSpacing(spacing
);
182 layout
->addStretch(100);
184 QVBoxLayout
*contentLayout
= new QVBoxLayout(this);
185 contentLayout
->addLayout(layout
);
186 if (m_contentWidget
) {
187 contentLayout
->addWidget(m_contentWidget
);
188 m_contentWidget
->setCursor(Qt::ArrowCursor
);
191 updateShowHideAttachmentState();
194 void AttachmentView::slotDownloadAttachment()
196 m_downloadAttachment
->setEnabled(false);
198 Imap::Network::FileDownloadManager
*manager
= new Imap::Network::FileDownloadManager(this, m_netAccess
, m_partIndex
);
199 connect(manager
, &Imap::Network::FileDownloadManager::fileNameRequested
, this, &AttachmentView::slotFileNameRequested
);
200 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, m_messageView
, &MessageView::transferError
);
201 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, this, &AttachmentView::enableDownloadAgain
);
202 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, manager
, &QObject::deleteLater
);
203 connect(manager
, &Imap::Network::FileDownloadManager::cancelled
, this, &AttachmentView::enableDownloadAgain
);
204 connect(manager
, &Imap::Network::FileDownloadManager::cancelled
, manager
, &QObject::deleteLater
);
205 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, this, &AttachmentView::enableDownloadAgain
);
206 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, manager
, &QObject::deleteLater
);
207 manager
->downloadPart();
210 void AttachmentView::slotOpenAttachment()
212 m_openAttachment
->setEnabled(false);
214 Imap::Network::FileDownloadManager
*manager
= new Imap::Network::FileDownloadManager(this, m_netAccess
, m_partIndex
);
215 connect(manager
, &Imap::Network::FileDownloadManager::fileNameRequested
, this, &AttachmentView::slotFileNameRequestedOnOpen
);
216 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, m_messageView
, &MessageView::transferError
);
217 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, this, &AttachmentView::onOpenFailed
);
218 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, manager
, &QObject::deleteLater
);
219 // we aren't connecting to cancelled() as it cannot really happen -- the filename is never empty
220 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, this, &AttachmentView::openDownloadedAttachment
);
221 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, manager
, &QObject::deleteLater
);
222 manager
->downloadPart();
225 void AttachmentView::slotFileNameRequestedOnOpen(QString
*fileName
)
227 Q_ASSERT(!m_tmpFile
);
228 m_tmpFile
= new QTemporaryFile(QDir::tempPath() + QLatin1String("/trojita-attachment-XXXXXX-") +
229 fileName
->replace(QLatin1Char('/'), QLatin1Char('_')));
230 m_tmpFile
->setAutoRemove(false);
232 *fileName
= m_tmpFile
->fileName();
235 void AttachmentView::slotFileNameRequested(QString
*fileName
)
237 static QDir lastDir
= QDir(Common::writablePath(Common::LOCATION_DOWNLOAD
));
238 if (!lastDir
.exists())
239 lastDir
= QDir(Common::writablePath(Common::LOCATION_DOWNLOAD
));
240 QString fileLocation
= lastDir
.filePath(*fileName
);
241 *fileName
= QFileDialog::getSaveFileName(this, tr("Save Attachment"), fileLocation
, QString(), nullptr, QFileDialog::HideNameFilterDetails
);
242 if (!fileName
->isEmpty())
243 lastDir
= QFileInfo(*fileName
).absoluteDir();
246 void AttachmentView::enableDownloadAgain()
248 m_downloadAttachment
->setEnabled(true);
251 void AttachmentView::onOpenFailed()
255 m_openAttachment
->setEnabled(true);
258 void AttachmentView::openDownloadedAttachment()
262 // Make sure that the file is read-only so that the launched application does not attempt to modify it
263 m_tmpFile
->setPermissions(QFile::ReadOwner
);
264 QDesktopServices::openUrl(QUrl::fromLocalFile(m_tmpFile
->fileName()));
267 m_openAttachment
->setEnabled(true);
270 bool AttachmentView::previewIsShown() const
272 return m_contentWidget
&& m_contentWidget
->isVisibleTo(const_cast<AttachmentView
*>(this));
275 void AttachmentView::updateShowHideAttachmentState()
277 if (m_showHideAttachment
) {
278 m_showHideAttachment
->setChecked(previewIsShown());
282 void AttachmentView::showMenuOrPreview()
284 if (previewIsShown() || !m_contentWidget
) {
287 m_showHideAttachment
->trigger();
291 void AttachmentView::showMenu()
293 if (QToolButton
*btn
= qobject_cast
<QToolButton
*>(sender())) {
296 QPoint p
= QCursor::pos();
297 p
.rx() -= m_menu
->width()/2;
301 void AttachmentView::toggleIconCursor()
303 if (m_icon
->isDown())
304 m_icon
->setCursor(Qt::OpenHandCursor
);
306 m_icon
->setCursor(Qt::ArrowCursor
);
309 void AttachmentView::indicateHover()
311 if (m_menu
->isVisible() || rect().contains(mapFromGlobal(QCursor::pos()))) { // WA_UnderMouse is wrong
312 if (!autoFillBackground()) {
313 setAutoFillBackground(true);
314 QPalette
pal(palette());
315 QLinearGradient
grad(0,0,0,height());
316 grad
.setColorAt(0, pal
.color(backgroundRole()));
317 grad
.setColorAt(0.15, pal
.color(backgroundRole()).lighter(110));
318 grad
.setColorAt(0.8, pal
.color(backgroundRole()).darker(110));
319 grad
.setColorAt(1, pal
.color(backgroundRole()));
320 pal
.setBrush(backgroundRole(), grad
);
324 setAutoFillBackground(false);
325 setPalette(QPalette());
329 void AttachmentView::mousePressEvent(QMouseEvent
*event
)
332 if (event
->button() == Qt::RightButton
) {
336 m_dragStartPos
= event
->pos();
337 QFrame::mousePressEvent(event
);
340 void AttachmentView::mouseMoveEvent(QMouseEvent
*event
)
342 QFrame::mouseMoveEvent(event
);
344 if (!(event
->buttons() & Qt::LeftButton
)) {
348 if ((m_dragStartPos
- event
->pos()).manhattanLength() < QApplication::startDragDistance())
351 QMimeData
*mimeData
= Imap::Mailbox::mimeDataForDragAndDrop(m_partIndex
);
355 QDrag
*drag
= new QDrag(this);
356 drag
->setMimeData(mimeData
);
357 drag
->setHotSpot(event
->pos());
358 drag
->exec(Qt::CopyAction
, Qt::CopyAction
);
361 void AttachmentView::paintEvent(QPaintEvent
*event
)
363 QFrame::paintEvent(event
);
365 const int x
= m_icon
->geometry().width() + m_fileName
->sizeHint().width() + 32;
366 if (x
>= rect().width())
368 QLinearGradient
grad(x
, 0, rect().right(), 0);
369 const QColor c
= testAttribute(Qt::WA_UnderMouse
) ? palette().color(QPalette::Highlight
) :
370 palette().color(backgroundRole()).darker(120);
371 grad
.setColorAt(0, palette().color(backgroundRole()));
372 grad
.setColorAt(0.5, c
);
373 grad
.setColorAt(1, palette().color(backgroundRole()));
376 p
.drawRect(x
, m_fileName
->geometry().center().y(), width(), 1);
380 QString
AttachmentView::quoteMe() const
382 const AbstractPartWidget
*widget
= dynamic_cast<const AbstractPartWidget
*>(m_contentWidget
);
383 return widget
&& !m_contentWidget
->isHidden() ? widget
->quoteMe() : QString();
386 bool AttachmentView::searchDialogRequested()
388 if (AbstractPartWidget
*widget
= dynamic_cast<AbstractPartWidget
*>(m_contentWidget
))
389 return widget
->searchDialogRequested();
393 #define IMPL_PART_FORWARD_ONE_METHOD(METHOD) \
394 void AttachmentView::METHOD() \
396 if (AbstractPartWidget *w = dynamic_cast<AbstractPartWidget*>(m_contentWidget)) \
400 IMPL_PART_FORWARD_ONE_METHOD(reloadContents
)
401 IMPL_PART_FORWARD_ONE_METHOD(zoomIn
)
402 IMPL_PART_FORWARD_ONE_METHOD(zoomOut
)
403 IMPL_PART_FORWARD_ONE_METHOD(zoomOriginal
)
405 void AttachmentView::showMessageSource()
407 auto w
= MainWindow::messageSourceWidget(m_partIndex
);
408 w
->setWindowTitle(tr("Source of Attached Message"));