Add credits for Marek
[trojita.git] / src / Gui / AttachmentView.cpp
blob99cc18907b4ed0f4c4399e92278af726f6e92179
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"
23 #include <QAction>
24 #include <QApplication>
25 #include <QDesktopServices>
26 #include <QDrag>
27 #include <QFileDialog>
28 #include <QHBoxLayout>
29 #include <QMenu>
30 #include <QMimeData>
31 #include <QMimeDatabase>
32 #include <QMouseEvent>
33 #include <QPainter>
34 #include <QPushButton>
35 #include <QLabel>
36 #include <QStyle>
37 #include <QStyleOption>
38 #include <QTemporaryFile>
39 #include <QTimer>
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"
52 namespace Gui
55 AttachmentView::AttachmentView(QWidget *parent, Imap::Network::MsgPartNetAccessManager *manager,
56 const QModelIndex &partIndex, MessageView *messageView, QWidget *contentWidget)
57 : QFrame(parent)
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)
65 , m_tmpFile(nullptr)
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
74 QStyleOption opt;
75 opt.initFrom(this);
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);
85 if (spacing < 0)
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);
107 // Icon on the left
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();
132 QIcon icon;
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"));
139 } else {
140 icon = QIcon::fromTheme(mimeType.iconName(),
141 QIcon::fromTheme(mimeType.genericIconName(), UiUtils::loadIcon(QStringLiteral("mail-attachment")))
144 m_icon->setIcon(icon);
145 } else {
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());
172 f.setItalic(true);
173 if (f.pointSize() > -1) // don't try the below on pixel fonts ever.
174 f.setPointSizeF(f.pointSizeF() * 0.8);
175 lbl->setFont(f);
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);
231 m_tmpFile->open();
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()
253 delete m_tmpFile;
254 m_tmpFile = nullptr;
255 m_openAttachment->setEnabled(true);
258 void AttachmentView::openDownloadedAttachment()
260 Q_ASSERT(m_tmpFile);
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()));
265 delete m_tmpFile;
266 m_tmpFile = nullptr;
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) {
285 showMenu();
286 } else {
287 m_showHideAttachment->trigger();
291 void AttachmentView::showMenu()
293 if (QToolButton *btn = qobject_cast<QToolButton*>(sender())) {
294 btn->setDown(false);
296 QPoint p = QCursor::pos();
297 p.rx() -= m_menu->width()/2;
298 m_menu->popup(p);
301 void AttachmentView::toggleIconCursor()
303 if (m_icon->isDown())
304 m_icon->setCursor(Qt::OpenHandCursor);
305 else
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);
321 setPalette(pal);
323 } else {
324 setAutoFillBackground(false);
325 setPalette(QPalette());
329 void AttachmentView::mousePressEvent(QMouseEvent *event)
331 event->accept();
332 if (event->button() == Qt::RightButton) {
333 showMenu();
334 return;
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)) {
345 return;
348 if ((m_dragStartPos - event->pos()).manhattanLength() < QApplication::startDragDistance())
349 return;
351 QMimeData *mimeData = Imap::Mailbox::mimeDataForDragAndDrop(m_partIndex);
352 if (!mimeData)
353 return;
354 event->accept();
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);
364 QPainter p(this);
365 const int x = m_icon->geometry().width() + m_fileName->sizeHint().width() + 32;
366 if (x >= rect().width())
367 return;
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()));
374 p.setBrush(grad);
375 p.setPen(Qt::NoPen);
376 p.drawRect(x, m_fileName->geometry().center().y(), width(), 1);
377 p.end();
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();
390 return false;
393 #define IMPL_PART_FORWARD_ONE_METHOD(METHOD) \
394 void AttachmentView::METHOD() \
396 if (AbstractPartWidget *w = dynamic_cast<AbstractPartWidget*>(m_contentWidget)) \
397 w->METHOD(); \
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"));
409 w->show();