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

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 "Common/SettingsNames.h"
0023 #include "EmbeddedWebView.h"
0024 #include "MessageView.h"
0025 #include "Gui/Util.h"
0026 
0027 #include <QAbstractScrollArea>
0028 #include <QAction>
0029 #include <QApplication>
0030 #include <QDesktopServices>
0031 #include <QDesktopWidget>
0032 #include <QLayout>
0033 #include <QMouseEvent>
0034 #include <QNetworkReply>
0035 #include <QScrollBar>
0036 #include <QStyle>
0037 #include <QStyleFactory>
0038 #include <QTimer>
0039 #include <QWebFrame>
0040 #include <QWebHistory>
0041 
0042 #include <QDebug>
0043 
0044 namespace {
0045 
0046 /** @short RAII pattern for counter manipulation */
0047 class Incrementor {
0048     int *m_int;
0049 public:
0050     Incrementor(int *what): m_int(what)
0051     {
0052         ++(*m_int);
0053     }
0054     ~Incrementor()
0055     {
0056         --(*m_int);
0057         Q_ASSERT(*m_int >= 0);
0058     }
0059 };
0060 
0061 }
0062 
0063 namespace Gui
0064 {
0065 
0066 EmbeddedWebView::EmbeddedWebView(QWidget *parent, QNetworkAccessManager *networkManager, QSettings *profileSettings)
0067     : QWebView(parent)
0068     , m_scrollParent(nullptr)
0069     , m_resizeInProgress(0)
0070     , m_staticWidth(0)
0071     , m_settings(profileSettings)
0072 {
0073     // set to expanding, ie. "freely" - this is important so the widget will attempt to shrink below the sizehint!
0074     setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0075     setFocusPolicy(Qt::StrongFocus); // not by the wheel
0076     setPage(new ErrorCheckingPage(this));
0077     page()->setNetworkAccessManager(networkManager);
0078 
0079     QWebSettings *s = settings();
0080     s->setAttribute(QWebSettings::JavascriptEnabled, false);
0081     s->setAttribute(QWebSettings::JavaEnabled, false);
0082     s->setAttribute(QWebSettings::PluginsEnabled, false);
0083     s->setAttribute(QWebSettings::PrivateBrowsingEnabled, true);
0084     s->setAttribute(QWebSettings::JavaEnabled, false);
0085     s->setAttribute(QWebSettings::OfflineStorageDatabaseEnabled, false);
0086     s->setAttribute(QWebSettings::OfflineWebApplicationCacheEnabled, false);
0087     s->setAttribute(QWebSettings::LocalStorageDatabaseEnabled, false);
0088     s->clearMemoryCaches();
0089 
0090     page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
0091     connect(this, &QWebView::linkClicked, this, &EmbeddedWebView::slotLinkClicked);
0092     connect(this, &QWebView::loadFinished, this, &EmbeddedWebView::handlePageLoadFinished);
0093     connect(page()->mainFrame(), &QWebFrame::contentsSizeChanged, this, &EmbeddedWebView::handlePageLoadFinished);
0094 
0095     // Scrolling is implemented on upper layers
0096     page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);
0097     page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);
0098 
0099     // Setup shortcuts for standard actions
0100     QAction *copyAction = page()->action(QWebPage::Copy);
0101     copyAction->setShortcut(tr("Ctrl+C"));
0102     addAction(copyAction);
0103 
0104     m_autoScrollTimer = new QTimer(this);
0105     m_autoScrollTimer->setInterval(50);
0106     connect(m_autoScrollTimer, &QTimer::timeout, this, &EmbeddedWebView::autoScroll);
0107 
0108     m_sizeContrainTimer = new QTimer(this);
0109     m_sizeContrainTimer->setInterval(50);
0110     m_sizeContrainTimer->setSingleShot(true);
0111     connect(m_sizeContrainTimer, &QTimer::timeout, this, &EmbeddedWebView::constrainSize);
0112 
0113     setContextMenuPolicy(Qt::NoContextMenu);
0114     findScrollParent();
0115 
0116     addCustomStylesheet(QString());
0117 
0118     loadColorScheme();
0119 }
0120 
0121 void EmbeddedWebView::constrainSize()
0122 {
0123     Incrementor dummy(&m_resizeInProgress);
0124 
0125     if (!(m_scrollParent && page() && page()->mainFrame()))
0126         return; // should not happen but who knows
0127 
0128     // Prevent expensive operation where a resize triggers one extra resizing operation.
0129     // This is very visible on large attachments, and in fact could possibly lead to recursion as the
0130     // contentsSizeChanged signal is connected to handlePageLoadFinished.
0131     if (m_resizeInProgress > 1)
0132         return;
0133 
0134     // the m_scrollParentPadding measures the summed up horizontal paddings of this view compared to
0135     // its m_scrollParent
0136     setMinimumSize(0,0);
0137     setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
0138     if (m_staticWidth) {
0139         resize(m_staticWidth, QWIDGETSIZE_MAX - 1);
0140         page()->setViewportSize(QSize(m_staticWidth, 32));
0141     } else {
0142         // resize so that the viewport has much vertical and wanted horizontal space
0143         resize(m_scrollParent->width() - m_scrollParentPadding, QWIDGETSIZE_MAX);
0144         // resize the PAGES viewport to this width and a minimum height
0145         page()->setViewportSize(QSize(m_scrollParent->width() - m_scrollParentPadding, 32));
0146     }
0147     // now the page has an idea about it's demanded size
0148     const QSize bestSize = page()->mainFrame()->contentsSize();
0149     // set the viewport to that size! - Otherwise it'd still be our "suggestion"
0150     page()->setViewportSize(bestSize);
0151     // fix the widgets size so the layout doesn't have much choice
0152     setFixedSize(bestSize);
0153     m_sizeContrainTimer->stop(); // we caused spurious resize events
0154 }
0155 
0156 void EmbeddedWebView::slotLinkClicked(const QUrl &url)
0157 {
0158     // Only allow external http:// and https:// links for safety reasons
0159     if (url.scheme().toLower() == QLatin1String("http") || url.scheme().toLower() == QLatin1String("https")) {
0160         QDesktopServices::openUrl(url);
0161     } else if (url.scheme().toLower() == QLatin1String("mailto")) {
0162         // The mailto: scheme is registered by Gui::MainWindow and handled internally;
0163         // even if it wasn't, opening a third-party application in response to a
0164         // user-initiated click does not pose a security risk
0165         QUrl betterUrl(url);
0166         betterUrl.setScheme(url.scheme().toLower());
0167         QDesktopServices::openUrl(betterUrl);
0168     }
0169 }
0170 
0171 void EmbeddedWebView::handlePageLoadFinished()
0172 {
0173     loadColorScheme();
0174     constrainSize();
0175 
0176     // We've already set it in our constructor, but apparently it isn't enough (Qt 4.8.0 on X11).
0177     // Let's do it again here, it works.
0178     Qt::ScrollBarPolicy policy = isWindow() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff;
0179     page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, policy);
0180     page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, policy);
0181     page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
0182 }
0183 
0184 void EmbeddedWebView::changeEvent(QEvent *e)
0185 {
0186     QWebView::changeEvent(e);
0187     if (e->type() == QEvent::ParentChange)
0188         findScrollParent();
0189 }
0190 
0191 bool EmbeddedWebView::eventFilter(QObject *o, QEvent *e)
0192 {
0193     if (o == m_scrollParent) {
0194         if (e->type() == QEvent::Resize) {
0195             if (!m_staticWidth)
0196                 m_sizeContrainTimer->start();
0197         } else if (e->type() == QEvent::Enter) {
0198             m_autoScrollPixels = 0;
0199             m_autoScrollTimer->stop();
0200         }
0201     }
0202     return QWebView::eventFilter(o, e);
0203 }
0204 
0205 void EmbeddedWebView::autoScroll()
0206 {
0207     if (!(m_scrollParent && m_autoScrollPixels)) {
0208         m_autoScrollPixels = 0;
0209         m_autoScrollTimer->stop();
0210         return;
0211     }
0212     if (QScrollBar *bar = static_cast<QAbstractScrollArea*>(m_scrollParent)->verticalScrollBar()) {
0213         bar->setValue(bar->value() + m_autoScrollPixels);
0214     }
0215 }
0216 
0217 void EmbeddedWebView::mouseMoveEvent(QMouseEvent *e)
0218 {
0219     if ((e->buttons() & Qt::LeftButton) && m_scrollParent) {
0220         m_autoScrollPixels = 0;
0221         const QPoint pos = mapTo(m_scrollParent, e->pos());
0222         if (pos.y() < 0)
0223             m_autoScrollPixels = pos.y();
0224         else if (pos.y() > m_scrollParent->rect().height())
0225             m_autoScrollPixels = pos.y() - m_scrollParent->rect().height();
0226         autoScroll();
0227         m_autoScrollTimer->start();
0228     }
0229     QWebView::mouseMoveEvent(e);
0230 }
0231 
0232 void EmbeddedWebView::mouseReleaseEvent(QMouseEvent *e)
0233 {
0234     if (!(e->buttons() & Qt::LeftButton)) {
0235         m_autoScrollPixels = 0;
0236         m_autoScrollTimer->stop();
0237     }
0238     QWebView::mouseReleaseEvent(e);
0239 }
0240 
0241 void EmbeddedWebView::findScrollParent()
0242 {
0243     if (m_scrollParent)
0244         m_scrollParent->removeEventFilter(this);
0245     m_scrollParent = 0;
0246     const int frameWidth = 2*style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
0247     m_scrollParentPadding = frameWidth;
0248     QWidget *runner = this;
0249     QMargins margins;
0250     while (runner) {
0251         runner->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
0252         margins = runner->contentsMargins();
0253         m_scrollParentPadding += margins.left() + margins.right() + frameWidth;
0254         if (runner->layout()) {
0255             runner->layout()->contentsMargins();
0256             m_scrollParentPadding += margins.left() + margins.right();
0257         }
0258         QWidget *p = runner->parentWidget();
0259         if (p && qobject_cast<MessageView*>(runner) && // is this a MessageView?
0260             p->objectName() == QLatin1String("qt_scrollarea_viewport") && // in a viewport?
0261             qobject_cast<QAbstractScrollArea*>(p->parentWidget())) { // that is used?
0262             margins = p->contentsMargins();
0263             m_scrollParentPadding += margins.left() + margins.right() + frameWidth;
0264             if (p->layout()) {
0265                 margins = p->layout()->contentsMargins();
0266                 m_scrollParentPadding += margins.left() + margins.right();
0267             }
0268             m_scrollParent = p->parentWidget();
0269             break; // then we have our actual message view
0270         }
0271         runner = p;
0272     }
0273     m_scrollParentPadding += style()->pixelMetric(QStyle::PM_ScrollBarExtent, 0, m_scrollParent);
0274     if (m_scrollParent)
0275         m_scrollParent->installEventFilter(this);
0276 }
0277 
0278 void EmbeddedWebView::showEvent(QShowEvent *se)
0279 {
0280     QWebView::showEvent(se);
0281     Qt::ScrollBarPolicy policy = isWindow() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff;
0282     page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAsNeeded);
0283     page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, policy);
0284     if (isWindow()) {
0285         resize(640,480);
0286     } else if (!m_scrollParent) // it would be much easier if the parents were just passed with the constructor ;-)
0287         findScrollParent();
0288 }
0289 
0290 QSize EmbeddedWebView::sizeHint() const
0291 {
0292     return QSize(32,32); // QWebView returns 800x600 what will lead to too wide pages for our implementation
0293 }
0294 
0295 QWidget *EmbeddedWebView::scrollParent() const
0296 {
0297     return m_scrollParent;
0298 }
0299 
0300 void EmbeddedWebView::setStaticWidth(int staticWidth)
0301 {
0302     m_staticWidth = staticWidth;
0303 }
0304 
0305 int EmbeddedWebView::staticWidth() const
0306 {
0307     return m_staticWidth;
0308 }
0309 
0310 ErrorCheckingPage::ErrorCheckingPage(QObject *parent): QWebPage(parent)
0311 {
0312 }
0313 
0314 bool ErrorCheckingPage::supportsExtension(Extension extension) const
0315 {
0316     if (extension == ErrorPageExtension)
0317         return true;
0318     else
0319         return false;
0320 }
0321 
0322 bool ErrorCheckingPage::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output)
0323 {
0324     if (extension != ErrorPageExtension)
0325         return false;
0326 
0327     const ErrorPageExtensionOption *input = static_cast<const ErrorPageExtensionOption *>(option);
0328     ErrorPageExtensionReturn *res = static_cast<ErrorPageExtensionReturn *>(output);
0329     if (input && res) {
0330         if (input->url.scheme() == QLatin1String("trojita-imap")) {
0331             QString emblem;
0332             if (input->domain == QtNetwork) {
0333                 switch (input->error) {
0334                 case QNetworkReply::TimeoutError:
0335                     emblem = QStringLiteral("network-disconnect");
0336                     break;
0337                 case QNetworkReply::ContentNotFoundError:
0338                     emblem = QStringLiteral("emblem-error");
0339                     break;
0340                 case QNetworkReply::UnknownProxyError:
0341                     emblem = QStringLiteral("emblem-error");
0342                     break;
0343                 }
0344             }
0345             if (!emblem.isNull()) {
0346                 res->content = QStringLiteral(
0347                             "<img src=\"%2\" style=\"vertical-align: middle\"/>"
0348                             "<span style=\"font-family: sans-serif; color: gray; margin-left: 0.5em\">%1</span>")
0349                         .arg(input->errorString, Util::resizedImageAsDataUrl(QStringLiteral(":/icons/%1.svg").arg(emblem), 32)).toUtf8();
0350                 return true;
0351             }
0352         }
0353         res->content = input->errorString.toUtf8();
0354         res->contentType = QStringLiteral("text/plain");
0355     }
0356     return true;
0357 }
0358 
0359 std::map<EmbeddedWebView::ColorScheme, QString> EmbeddedWebView::supportedColorSchemes()
0360 {
0361     std::map<EmbeddedWebView::ColorScheme, QString> map;
0362     map[ColorScheme::System] = tr("System colors");
0363     map[ColorScheme::AdjustedSystem] = tr("System theme adjusted for better contrast");
0364     map[ColorScheme::BlackOnWhite] = tr("Black on white, forced");
0365     return map;
0366 }
0367 
0368 void EmbeddedWebView::setColorScheme(const ColorScheme colorScheme)
0369 {
0370     m_colorScheme = colorScheme;
0371     addCustomStylesheet(m_customCss);
0372 }
0373 
0374 /**
0375  * @brief EmbeddedWebView::loadtColorScheme loads and applies a color scheme setting from the profile config.
0376  */
0377 void EmbeddedWebView::loadColorScheme()
0378 {
0379     ColorScheme schemeId = m_settings->value(Common::SettingsNames::msgViewColorScheme,  QVariant::fromValue(ColorScheme::System)).value<ColorScheme>();
0380     setColorScheme(schemeId);
0381 }
0382 
0383 /**
0384  * @brief EmbeddedWebView::changeColorScheme saves and applies passed configuration of a color scheme setting.
0385  */
0386 void EmbeddedWebView::changeColorScheme(const ColorScheme colorScheme)
0387 {
0388     m_settings->setValue(Common::SettingsNames::msgViewColorScheme, QVariant::fromValue(colorScheme).toInt());
0389     setColorScheme(colorScheme);
0390 }
0391 
0392 void EmbeddedWebView::addCustomStylesheet(const QString &css)
0393 {
0394     m_customCss = css;
0395 
0396     QWebSettings *s = settings();
0397     QString bgName, fgName;
0398     QColor bg = palette().color(QPalette::Active, QPalette::Base),
0399            fg = palette().color(QPalette::Active, QPalette::Text);
0400 
0401     switch (m_colorScheme) {
0402     case ColorScheme::BlackOnWhite:
0403         bgName = QStringLiteral("white !important");
0404         fgName = QStringLiteral("black !important");
0405         break;
0406     case ColorScheme::AdjustedSystem:
0407     {
0408         // This is HTML, and the authors of that markup are free to specify only the background colors, or only the foreground colors.
0409         // No matter what we pass from outside, there will always be some color which will result in unreadable text, and we can do
0410         // nothing except adding !important everywhere to fix this.
0411         // This code attempts to create a color which will try to produce exactly ugly results for both dark-on-bright and
0412         // bright-on-dark segments of text. However, it's pure alchemy and only a limited heuristics. If you do not like this, please
0413         // submit patches (or talk to the HTML producers, hehehe).
0414         const int v = bg.value();
0415         if (v < 96 && fg.value() > 128 + v/2) {
0416             int h,s,vv,a;
0417             fg.getHsv(&h, &s, &vv, &a) ;
0418             fg.setHsv(h, s, 128+v/2, a);
0419         }
0420         bgName = bg.name();
0421         fgName = fg.name();
0422         break;
0423     }
0424     case ColorScheme::System:
0425         bgName = bg.name();
0426         fgName = fg.name();
0427         break;
0428     }
0429 
0430 
0431     const QString urlPrefix(QStringLiteral("data:text/css;charset=utf-8;base64,"));
0432     const QString myColors(QStringLiteral("body { background-color: %1; color: %2; }\n").arg(bgName, fgName));
0433     s->setUserStyleSheetUrl(QString::fromUtf8(urlPrefix.toUtf8() + (myColors + m_customCss).toUtf8().toBase64()));
0434 }
0435 
0436 }