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/DeleteAfter.h"
43 #include "Common/Paths.h"
44 #include "Gui/MessageView.h" // so that the compiler knows it's a QObject
45 #include "Gui/Window.h"
46 #include "Imap/Network/FileDownloadManager.h"
47 #include "Imap/Model/DragAndDrop.h"
48 #include "Imap/Model/MailboxTree.h"
49 #include "Imap/Model/ItemRoles.h"
50 #include "UiUtils/Formatting.h"
51 #include "UiUtils/IconLoader.h"
56 AttachmentView::AttachmentView(QWidget
*parent
, Imap::Network::MsgPartNetAccessManager
*manager
,
57 const QModelIndex
&partIndex
, MessageView
*messageView
, QWidget
*contentWidget
)
59 , m_partIndex(partIndex
)
60 , m_messageView(messageView
)
61 , m_downloadAttachment(nullptr)
62 , m_openAttachment(nullptr)
63 , m_showHideAttachment(nullptr)
64 , m_showSource(nullptr)
65 , m_netAccess(manager
)
67 , m_contentWidget(contentWidget
)
69 setSizePolicy(QSizePolicy::Preferred
, QSizePolicy::Fixed
);
70 setFrameStyle(QFrame::NoFrame
);
71 setCursor(Qt::OpenHandCursor
);
72 setAttribute(Qt::WA_Hover
);
74 // not actually required, but styles may assume the parameter and segfault on nullptr deref
78 const int padding
= style()->pixelMetric(QStyle::PM_DefaultFrameWidth
, &opt
, this);
79 setContentsMargins(padding
, 0, padding
, 0);
81 QHBoxLayout
*layout
= new QHBoxLayout();
82 layout
->setContentsMargins(0,0,0,0);
84 // should be PM_LayoutHorizontalSpacing, but is not implemented by many Qt4 styles -including oxygen- for other conflicts
85 int spacing
= style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing
, &opt
, this);
87 spacing
= style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing
, &opt
, this);
88 layout
->setSpacing(0);
90 m_menu
= new QMenu(this);
91 m_downloadAttachment
= m_menu
->addAction(UiUtils::loadIcon(QStringLiteral("document-save-as")), tr("Download"));
92 m_openAttachment
= m_menu
->addAction(tr("Open Directly"));
93 connect(m_downloadAttachment
, &QAction::triggered
, this, &AttachmentView::slotDownloadAttachment
);
94 connect(m_openAttachment
, &QAction::triggered
, this, &AttachmentView::slotOpenAttachment
);
95 connect(m_menu
, &QMenu::aboutToShow
, this, &AttachmentView::updateShowHideAttachmentState
);
96 if (m_contentWidget
) {
97 m_showHideAttachment
= m_menu
->addAction(UiUtils::loadIcon(QStringLiteral("view-preview")), tr("Show Preview"));
98 m_showHideAttachment
->setCheckable(true);
99 m_showHideAttachment
->setChecked(!m_contentWidget
->isHidden());
100 connect(m_showHideAttachment
, &QAction::triggered
, m_contentWidget
, &QWidget::setVisible
);
101 connect(m_showHideAttachment
, &QAction::triggered
, this, &AttachmentView::updateShowHideAttachmentState
);
103 if (partIndex
.data(Imap::Mailbox::RolePartMimeType
).toByteArray() == "message/rfc822") {
104 m_showSource
= m_menu
->addAction(UiUtils::loadIcon(QStringLiteral("text-x-hex")), tr("Show Message Source"));
105 connect(m_showSource
, &QAction::triggered
, this, &AttachmentView::showMessageSource
);
109 m_icon
= new QToolButton(this);
110 m_icon
->setAttribute(Qt::WA_NoMousePropagation
, false); // inform us for DnD
111 m_icon
->setAutoRaise(true);
112 m_icon
->setIconSize(QSize(22,22));
113 m_icon
->setToolButtonStyle(Qt::ToolButtonIconOnly
);
114 m_icon
->setPopupMode(QToolButton::MenuButtonPopup
);
115 m_icon
->setMenu(m_menu
);
116 connect(m_icon
, &QAbstractButton::pressed
, this, &AttachmentView::toggleIconCursor
);
117 connect(m_icon
, &QAbstractButton::clicked
, this, &AttachmentView::showMenuOrPreview
);
118 connect(m_icon
, &QAbstractButton::released
, this, &AttachmentView::toggleIconCursor
);
119 m_icon
->setCursor(Qt::ArrowCursor
);
121 QString mimeDescription
= partIndex
.data(Imap::Mailbox::RolePartMimeType
).toString();
122 QString rawMime
= mimeDescription
;
123 QMimeType mimeType
= QMimeDatabase().mimeTypeForName(mimeDescription
);
124 if (mimeType
.isValid() && !mimeType
.isDefault()) {
125 mimeDescription
= mimeType
.comment();
127 if (rawMime
== QLatin1String("message/rfc822")) {
128 // Special case for plain e-mail messages. Motivation for this is that most of the OSes ship these icons
129 // with a pixmap which shows something like a sheet of paper as the background. I find it rather dumb
130 // to do this in the context of a MUA where attached messages are pretty common, which is why this special
131 // case is in place. Comments welcome.
132 icon
= UiUtils::loadIcon(QStringLiteral("trojita"));
134 icon
= QIcon::fromTheme(mimeType
.iconName(),
135 QIcon::fromTheme(mimeType
.genericIconName(), UiUtils::loadIcon(QStringLiteral("mail-attachment")))
138 m_icon
->setIcon(icon
);
140 m_icon
->setIcon(UiUtils::loadIcon(QStringLiteral("mail-attachment")));
143 layout
->addWidget(m_icon
);
145 // space between icon and label
146 layout
->addSpacing(spacing
);
148 QVBoxLayout
*subLayout
= new QVBoxLayout
;
149 subLayout
->setContentsMargins(0,0,0,0);
150 // The file name shall be mouse-selectable
151 m_fileName
= new QLabel(this);
152 m_fileName
->setTextFormat(Qt::PlainText
);
153 m_fileName
->setText(partIndex
.data(Imap::Mailbox::RolePartFileName
).toString());
154 m_fileName
->setTextInteractionFlags(Qt::TextSelectableByMouse
);
155 m_fileName
->setCursor(Qt::IBeamCursor
);
156 m_fileName
->setSizePolicy(QSizePolicy::Fixed
, QSizePolicy::Fixed
);
157 subLayout
->addWidget(m_fileName
);
159 // Some metainformation -- the MIME type and the file size
160 QLabel
*lbl
= new QLabel(tr("%2, %3").arg(mimeDescription
,
161 UiUtils::Formatting::prettySize(partIndex
.data(Imap::Mailbox::RolePartOctets
).toULongLong())));
162 if (rawMime
!= mimeDescription
) {
163 lbl
->setToolTip(rawMime
);
165 QFont
f(lbl
->font());
167 if (f
.pointSize() > -1) // don't try the below on pixel fonts ever.
168 f
.setPointSizeF(f
.pointSizeF() * 0.8);
170 subLayout
->addWidget(lbl
);
171 layout
->addLayout(subLayout
);
173 // space between label and arrow
174 layout
->addSpacing(spacing
);
176 layout
->addStretch(100);
178 QVBoxLayout
*contentLayout
= new QVBoxLayout(this);
179 contentLayout
->addLayout(layout
);
180 if (m_contentWidget
) {
181 contentLayout
->addWidget(m_contentWidget
);
182 m_contentWidget
->setCursor(Qt::ArrowCursor
);
185 updateShowHideAttachmentState();
188 void AttachmentView::slotDownloadAttachment()
190 m_downloadAttachment
->setEnabled(false);
192 Imap::Network::FileDownloadManager
*manager
= new Imap::Network::FileDownloadManager(this, m_netAccess
, m_partIndex
);
193 connect(manager
, &Imap::Network::FileDownloadManager::fileNameRequested
, this, &AttachmentView::slotFileNameRequested
);
194 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, m_messageView
, &MessageView::transferError
);
195 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, this, &AttachmentView::enableDownloadAgain
);
196 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, manager
, &QObject::deleteLater
);
197 connect(manager
, &Imap::Network::FileDownloadManager::cancelled
, this, &AttachmentView::enableDownloadAgain
);
198 connect(manager
, &Imap::Network::FileDownloadManager::cancelled
, manager
, &QObject::deleteLater
);
199 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, this, &AttachmentView::enableDownloadAgain
);
200 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, manager
, &QObject::deleteLater
);
201 manager
->downloadPart();
204 void AttachmentView::slotOpenAttachment()
206 m_openAttachment
->setEnabled(false);
208 Imap::Network::FileDownloadManager
*manager
= new Imap::Network::FileDownloadManager(this, m_netAccess
, m_partIndex
);
209 connect(manager
, &Imap::Network::FileDownloadManager::fileNameRequested
, this, &AttachmentView::slotFileNameRequestedOnOpen
);
210 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, m_messageView
, &MessageView::transferError
);
211 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, this, &AttachmentView::onOpenFailed
);
212 connect(manager
, &Imap::Network::FileDownloadManager::transferError
, manager
, &QObject::deleteLater
);
213 // we aren't connecting to cancelled() as it cannot really happen -- the filename is never empty
214 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, this, &AttachmentView::openDownloadedAttachment
);
215 connect(manager
, &Imap::Network::FileDownloadManager::succeeded
, manager
, &QObject::deleteLater
);
216 manager
->downloadPart();
219 void AttachmentView::slotFileNameRequestedOnOpen(QString
*fileName
)
221 Q_ASSERT(!m_tmpFile
);
222 m_tmpFile
= new QTemporaryFile(QDir::tempPath() + QLatin1String("/trojita-attachment-XXXXXX-") +
223 fileName
->replace(QLatin1Char('/'), QLatin1Char('_')));
225 *fileName
= m_tmpFile
->fileName();
228 void AttachmentView::slotFileNameRequested(QString
*fileName
)
230 static QDir lastDir
= QDir(Common::writablePath(Common::LOCATION_DOWNLOAD
));
231 if (!lastDir
.exists())
232 lastDir
= QDir(Common::writablePath(Common::LOCATION_DOWNLOAD
));
233 QString fileLocation
= lastDir
.filePath(*fileName
);
234 *fileName
= QFileDialog::getSaveFileName(this, tr("Save Attachment"), fileLocation
, QString(), nullptr, QFileDialog::HideNameFilterDetails
);
235 if (!fileName
->isEmpty())
236 lastDir
= QFileInfo(*fileName
).absoluteDir();
239 void AttachmentView::enableDownloadAgain()
241 m_downloadAttachment
->setEnabled(true);
244 void AttachmentView::onOpenFailed()
248 m_openAttachment
->setEnabled(true);
251 void AttachmentView::openDownloadedAttachment()
255 // Make sure that the file is read-only so that the launched application does not attempt to modify it
256 m_tmpFile
->setPermissions(QFile::ReadOwner
);
258 QDesktopServices::openUrl(QUrl::fromLocalFile(m_tmpFile
->fileName()));
260 // This will delete the temporary file in ten seconds. It should give the application plenty of time to start and also prevent
261 // leaving cruft behind.
262 new Common::DeleteAfter(m_tmpFile
, 10000);
264 m_openAttachment
->setEnabled(true);
267 bool AttachmentView::previewIsShown() const
269 return m_contentWidget
&& m_contentWidget
->isVisibleTo(const_cast<AttachmentView
*>(this));
272 void AttachmentView::updateShowHideAttachmentState()
274 if (m_showHideAttachment
) {
275 m_showHideAttachment
->setChecked(previewIsShown());
279 void AttachmentView::showMenuOrPreview()
281 if (previewIsShown() || !m_contentWidget
) {
284 m_showHideAttachment
->trigger();
288 void AttachmentView::showMenu()
290 if (QToolButton
*btn
= qobject_cast
<QToolButton
*>(sender())) {
293 QPoint p
= QCursor::pos();
294 p
.rx() -= m_menu
->width()/2;
298 void AttachmentView::toggleIconCursor()
300 if (m_icon
->isDown())
301 m_icon
->setCursor(Qt::OpenHandCursor
);
303 m_icon
->setCursor(Qt::ArrowCursor
);
306 void AttachmentView::indicateHover()
308 if (m_menu
->isVisible() || rect().contains(mapFromGlobal(QCursor::pos()))) { // WA_UnderMouse is wrong
309 if (!autoFillBackground()) {
310 setAutoFillBackground(true);
311 QPalette
pal(palette());
312 QLinearGradient
grad(0,0,0,height());
313 grad
.setColorAt(0, pal
.color(backgroundRole()));
314 grad
.setColorAt(0.15, pal
.color(backgroundRole()).lighter(110));
315 grad
.setColorAt(0.8, pal
.color(backgroundRole()).darker(110));
316 grad
.setColorAt(1, pal
.color(backgroundRole()));
317 pal
.setBrush(backgroundRole(), grad
);
321 setAutoFillBackground(false);
322 setPalette(QPalette());
326 void AttachmentView::mousePressEvent(QMouseEvent
*event
)
329 if (event
->button() == Qt::RightButton
) {
333 m_dragStartPos
= event
->pos();
334 QFrame::mousePressEvent(event
);
337 void AttachmentView::mouseMoveEvent(QMouseEvent
*event
)
339 QFrame::mouseMoveEvent(event
);
341 if (!(event
->buttons() & Qt::LeftButton
)) {
345 if ((m_dragStartPos
- event
->pos()).manhattanLength() < QApplication::startDragDistance())
348 QMimeData
*mimeData
= Imap::Mailbox::mimeDataForDragAndDrop(m_partIndex
);
352 QDrag
*drag
= new QDrag(this);
353 drag
->setMimeData(mimeData
);
354 drag
->setHotSpot(event
->pos());
355 drag
->exec(Qt::CopyAction
, Qt::CopyAction
);
358 void AttachmentView::paintEvent(QPaintEvent
*event
)
360 QFrame::paintEvent(event
);
362 const int x
= m_icon
->geometry().width() + m_fileName
->sizeHint().width() + 32;
363 if (x
>= rect().width())
365 QLinearGradient
grad(x
, 0, rect().right(), 0);
366 const QColor c
= testAttribute(Qt::WA_UnderMouse
) ? palette().color(QPalette::Highlight
) :
367 palette().color(backgroundRole()).darker(120);
368 grad
.setColorAt(0, palette().color(backgroundRole()));
369 grad
.setColorAt(0.5, c
);
370 grad
.setColorAt(1, palette().color(backgroundRole()));
373 p
.drawRect(x
, m_fileName
->geometry().center().y(), width(), 1);
377 QString
AttachmentView::quoteMe() const
379 const AbstractPartWidget
*widget
= dynamic_cast<const AbstractPartWidget
*>(m_contentWidget
);
380 return widget
&& !m_contentWidget
->isHidden() ? widget
->quoteMe() : QString();
383 bool AttachmentView::searchDialogRequested()
385 if (AbstractPartWidget
*widget
= dynamic_cast<AbstractPartWidget
*>(m_contentWidget
))
386 return widget
->searchDialogRequested();
390 #define IMPL_PART_FORWARD_ONE_METHOD(METHOD) \
391 void AttachmentView::METHOD() \
393 if (AbstractPartWidget *w = dynamic_cast<AbstractPartWidget*>(m_contentWidget)) \
397 IMPL_PART_FORWARD_ONE_METHOD(reloadContents
)
398 IMPL_PART_FORWARD_ONE_METHOD(zoomIn
)
399 IMPL_PART_FORWARD_ONE_METHOD(zoomOut
)
400 IMPL_PART_FORWARD_ONE_METHOD(zoomOriginal
)
402 void AttachmentView::showMessageSource()
404 auto w
= MainWindow::messageSourceWidget(m_partIndex
);
405 w
->setWindowTitle(tr("Source of Attached Message"));