File indexing completed on 2024-06-16 05:01:30

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 #include <QApplication>
0023 #include <QClipboard>
0024 #include <QFileDialog>
0025 #include <QFontDatabase>
0026 #include <QMenu>
0027 #include <QNetworkReply>
0028 #include <QWebFrame>
0029 #include <QWheelEvent>
0030 
0031 #include "SimplePartWidget.h"
0032 #include "Common/MetaTypes.h"
0033 #include "Common/Paths.h"
0034 #include "Composer/Mailto.h"
0035 #include "Gui/MessageView.h" // so that the compiler knows that it's a QObject
0036 #include "Gui/Util.h"
0037 #include "Imap/Encoders.h"
0038 #include "Imap/Model/ItemRoles.h"
0039 #include "Imap/Model/MailboxTree.h"
0040 #include "Imap/Model/Model.h"
0041 #include "Imap/Network/FileDownloadManager.h"
0042 #include "Imap/Model/Utils.h"
0043 #include "UiUtils/Color.h"
0044 #include "UiUtils/IconLoader.h"
0045 
0046 namespace Gui
0047 {
0048 
0049 SimplePartWidget::SimplePartWidget(QWidget *parent, Imap::Network::MsgPartNetAccessManager *manager,
0050                                    const QModelIndex &partIndex, MessageView *messageView):
0051     EmbeddedWebView(parent, manager, messageView->profileSettings()), m_partIndex(partIndex), m_messageView(messageView), m_netAccessManager(manager)
0052 {
0053     Q_ASSERT(partIndex.isValid());
0054 
0055     if (m_messageView) {
0056         connect(this, &QWebView::loadStarted, m_messageView, &MessageView::onWebViewLoadStarted);
0057         connect(this, &QWebView::loadFinished, m_messageView, &MessageView::onWebViewLoadFinished);
0058     }
0059 
0060     QUrl url;
0061     url.setScheme(QStringLiteral("trojita-imap"));
0062     url.setHost(QStringLiteral("msg"));
0063     url.setPath(partIndex.data(Imap::Mailbox::RolePartPathToPart).toString());
0064     if (partIndex.data(Imap::Mailbox::RolePartMimeType).toString() == QLatin1String("text/plain")) {
0065         if (partIndex.data(Imap::Mailbox::RolePartOctets).toULongLong() < 100 * 1024) {
0066             connect(this, &QWebView::loadFinished, this, &SimplePartWidget::slotMarkupPlainText);
0067         } else {
0068             QFont font(QFontDatabase::systemFont(QFontDatabase::FixedFont));
0069             setStaticWidth(QFontMetrics(font).maxWidth()*90);
0070             addCustomStylesheet(QStringLiteral("pre{word-wrap:normal !important;white-space:pre !important;}"));
0071             QWebSettings *s = settings();
0072             s->setFontFamily(QWebSettings::StandardFont, font.family());
0073         }
0074     }
0075     load(url);
0076 
0077     m_savePart = new QAction(UiUtils::loadIcon(QStringLiteral("document-save")), tr("Save this message part..."), this);
0078     connect(m_savePart, &QAction::triggered, this, &SimplePartWidget::slotDownloadPart);
0079     this->addAction(m_savePart);
0080 
0081     m_saveMessage = new QAction(UiUtils::loadIcon(QStringLiteral("document-save-all")), tr("Save whole message..."), this);
0082     connect(m_saveMessage, &QAction::triggered, this, &SimplePartWidget::slotDownloadMessage);
0083     this->addAction(m_saveMessage);
0084 
0085     m_findAction = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find")), tr("Search..."), this);
0086     connect(m_findAction, &QAction::triggered, this, &SimplePartWidget::searchDialogRequested);
0087 
0088     m_copyMail = new QAction(UiUtils::loadIcon(QStringLiteral("edit-copy")), tr("Copy e-mail address"), this);
0089     connect(m_copyMail, &QAction::triggered, this, [this](){
0090         QGuiApplication::clipboard()->setText(m_copyMail->data().toString());
0091     });
0092     this->addAction(m_copyMail);
0093 
0094     setContextMenuPolicy(Qt::CustomContextMenu);
0095 
0096     // It is actually OK to construct this widget without any connection to a messageView -- this is often used when
0097     // displaying message source or message headers. Let's silence the QObject::connect warning.
0098     if (m_messageView) {
0099         connect(this, &QWidget::customContextMenuRequested, m_messageView, &MessageView::partContextMenuRequested);
0100         // The targets expect the sender() of the signal to be a SimplePartWidget, not a QWebPage,
0101         // which means we have to do this indirection
0102         connect(page(), &QWebPage::linkHovered, this, &SimplePartWidget::linkHovered);
0103         connect(this, &SimplePartWidget::linkHovered, m_messageView, &MessageView::partLinkHovered);
0104         connect(page(), &QWebPage::downloadRequested, this, &SimplePartWidget::slotDownloadImage);
0105 
0106         installEventFilter(m_messageView);
0107     }
0108 }
0109 
0110 void SimplePartWidget::slotMarkupPlainText()
0111 {
0112     // NOTICE "single shot", we get a recursion otherwise!
0113     disconnect(this, &QWebView::loadFinished, this, &SimplePartWidget::slotMarkupPlainText);
0114 
0115     // If there's no data, don't try to "fix it up"
0116     if (!m_partIndex.isValid() || !m_partIndex.data(Imap::Mailbox::RoleIsFetched).toBool())
0117         return;
0118 
0119     QPalette palette = QApplication::palette();
0120 
0121     // and finally set the marked up page.
0122     page()->mainFrame()->setHtml(UiUtils::htmlizedTextPart(m_partIndex, QFontDatabase::systemFont(QFontDatabase::FixedFont),
0123                                                            palette.base().color(), palette.text().color(),
0124                                                            palette.link().color(), palette.linkVisited().color()));
0125 }
0126 
0127 void SimplePartWidget::slotFileNameRequested(QString *fileName)
0128 {
0129     *fileName = QFileDialog::getSaveFileName(this, tr("Save Attachment"),
0130                 *fileName, QString(),
0131                 0, QFileDialog::HideNameFilterDetails
0132                                             );
0133 }
0134 
0135 QString SimplePartWidget::quoteMe() const
0136 {
0137     QString selection = selectedText();
0138     if (selection.isEmpty())
0139         return page()->mainFrame()->toPlainText();
0140     else
0141         return selection;
0142 }
0143 
0144 void SimplePartWidget::reloadContents()
0145 {
0146     EmbeddedWebView::reload();
0147 }
0148 
0149 const auto zoomConstant = 1.1;
0150 
0151 void SimplePartWidget::zoomIn()
0152 {
0153     setZoomFactor(zoomFactor() * zoomConstant);
0154     constrainSize();
0155 }
0156 
0157 void SimplePartWidget::zoomOut()
0158 {
0159     setZoomFactor(zoomFactor() / zoomConstant);
0160     constrainSize();
0161 }
0162 
0163 void SimplePartWidget::zoomOriginal()
0164 {
0165     setZoomFactor(1);
0166     constrainSize();
0167 }
0168 
0169 bool SimplePartWidget::searchDialogRequested()
0170 {
0171     m_messageView->triggerSearchDialogBy(this);
0172     return true;
0173 }
0174 
0175 void SimplePartWidget::buildContextMenu(const QPoint &point, QMenu &menu) const
0176 {
0177     menu.addAction(m_findAction);
0178     auto a = pageAction(QWebPage::Copy);
0179     a->setIcon(UiUtils::loadIcon(QStringLiteral("edit-copy")));
0180     menu.addAction(a);
0181     a = pageAction(QWebPage::SelectAll);
0182     a->setIcon(UiUtils::loadIcon(QStringLiteral("edit-select-all")));
0183     menu.addAction(a);
0184     auto linkUrl = page()->mainFrame()->hitTestContent(point).linkUrl();
0185     if (!linkUrl.isEmpty()) {
0186         menu.addSeparator();
0187         auto oneMail = Composer::extractOneMailAddress(linkUrl);
0188         if (!oneMail.isEmpty()) {
0189             a = m_copyMail;
0190             a->setData(oneMail);
0191         } else {
0192             a = pageAction(QWebPage::CopyLinkToClipboard);
0193             a->setIcon(UiUtils::loadIcon(QStringLiteral("edit-copy")));
0194         }
0195         menu.addAction(a);
0196     }
0197     menu.addSeparator();
0198     menu.addAction(m_savePart);
0199     menu.addAction(m_saveMessage);
0200     if (!page()->mainFrame()->hitTestContent(point).imageUrl().isEmpty()) {
0201         a = pageAction(QWebPage::DownloadImageToDisk);
0202         a->setIcon(UiUtils::loadIcon(QStringLiteral("download")));
0203         menu.addAction(a);
0204     }
0205     menu.addSeparator();
0206     QMenu *colorSchemeMenu = menu.addMenu(UiUtils::loadIcon(QStringLiteral("colorneg")), tr("Color scheme"));
0207     QActionGroup *ag = new QActionGroup(colorSchemeMenu);
0208     for (auto item: supportedColorSchemes()) {
0209         QAction *a = colorSchemeMenu->addAction(item.second);
0210         connect(a, &QAction::triggered, this, [this, item](){
0211            const_cast<SimplePartWidget*>(this)->changeColorScheme(item.first);
0212         });
0213         a->setCheckable(true);
0214         if (item.first == m_colorScheme) {
0215             a->setChecked(true);
0216         }
0217         a->setActionGroup(ag);
0218     }
0219 
0220     auto zoomMenu = menu.addMenu(UiUtils::loadIcon(QStringLiteral("zoom")), tr("Zoom"));
0221     if (m_messageView) {
0222         zoomMenu->addAction(m_messageView->m_zoomIn);
0223         zoomMenu->addAction(m_messageView->m_zoomOut);
0224         zoomMenu->addAction(m_messageView->m_zoomOriginal);
0225     } else {
0226         auto zoomIn = zoomMenu->addAction(UiUtils::loadIcon(QStringLiteral("zoom-in")), tr("Zoom In"));
0227         zoomIn->setShortcut(QKeySequence::ZoomIn);
0228         connect(zoomIn, &QAction::triggered, this, &SimplePartWidget::zoomIn);
0229 
0230         auto zoomOut = zoomMenu->addAction(UiUtils::loadIcon(QStringLiteral("zoom-out")), tr("Zoom Out"));
0231         zoomOut->setShortcut(QKeySequence::ZoomOut);
0232         connect(zoomOut, &QAction::triggered, this, &SimplePartWidget::zoomOut);
0233 
0234         auto zoomOriginal = zoomMenu->addAction(UiUtils::loadIcon(QStringLiteral("zoom-original")), tr("Original Size"));
0235         connect(zoomOriginal, &QAction::triggered, this, &SimplePartWidget::zoomOriginal);
0236     }
0237 }
0238 
0239 void SimplePartWidget::slotDownloadPart()
0240 {
0241     Imap::Network::FileDownloadManager *manager = new Imap::Network::FileDownloadManager(this, m_netAccessManager, m_partIndex);
0242     connect(manager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &SimplePartWidget::slotFileNameRequested);
0243     connect(manager, &Imap::Network::FileDownloadManager::transferError, m_messageView, &MessageView::transferError);
0244     connect(manager, &Imap::Network::FileDownloadManager::transferError, manager, &QObject::deleteLater);
0245     connect(manager, &Imap::Network::FileDownloadManager::succeeded, manager, &QObject::deleteLater);
0246     manager->downloadPart();
0247 }
0248 
0249 void SimplePartWidget::slotDownloadMessage()
0250 {
0251     QModelIndex index = m_partIndex.data(Imap::Mailbox::RolePartMessageIndex).toModelIndex();
0252     Imap::Network::FileDownloadManager *manager = new Imap::Network::FileDownloadManager(this, m_netAccessManager, index);
0253     connect(manager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &SimplePartWidget::slotFileNameRequested);
0254     connect(manager, &Imap::Network::FileDownloadManager::transferError, m_messageView, &MessageView::transferError);
0255     connect(manager, &Imap::Network::FileDownloadManager::transferError, manager, &QObject::deleteLater);
0256     connect(manager, &Imap::Network::FileDownloadManager::succeeded, manager, &QObject::deleteLater);
0257     manager->downloadMessage();
0258 }
0259 
0260 void SimplePartWidget::slotDownloadImage(const QNetworkRequest &req)
0261 {
0262     Imap::Network::FileDownloadManager *manager = new Imap::Network::FileDownloadManager(this, m_netAccessManager, req.url(), m_partIndex.parent());
0263     connect(manager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &SimplePartWidget::slotFileNameRequested);
0264     connect(manager, &Imap::Network::FileDownloadManager::transferError, m_messageView, &MessageView::transferError);
0265     connect(manager, &Imap::Network::FileDownloadManager::transferError, manager, &QObject::deleteLater);
0266     connect(manager, &Imap::Network::FileDownloadManager::succeeded, manager, &QObject::deleteLater);
0267     manager->downloadPart();
0268 }
0269 
0270 }
0271