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 }