1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2 
3    This file is part of the Trojita Qt IMAP e-mail client,
4    http://trojita.flaska.net/
5 
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.
13 
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.
18 
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/>.
21 */
22 #include "EmbeddedWebView.h"
23 #include "MessageView.h"
24 #include "Gui/Util.h"
25 
26 #include <QAbstractScrollArea>
27 #include <QAction>
28 #include <QApplication>
29 #include <QDesktopServices>
30 #include <QDesktopWidget>
31 #include <QLayout>
32 #include <QMouseEvent>
33 #include <QNetworkReply>
34 #include <QScrollBar>
35 #include <QStyle>
36 #include <QStyleFactory>
37 #include <QTimer>
38 #include <QWebFrame>
39 #include <QWebHistory>
40 
41 #include <QDebug>
42 
43 namespace {
44 
45 /** @short RAII pattern for counter manipulation */
46 class Incrementor {
47     int *m_int;
48 public:
Incrementor(int * what)49     Incrementor(int *what): m_int(what)
50     {
51         ++(*m_int);
52     }
~Incrementor()53     ~Incrementor()
54     {
55         --(*m_int);
56         Q_ASSERT(*m_int >= 0);
57     }
58 };
59 
60 }
61 
62 namespace Gui
63 {
64 
EmbeddedWebView(QWidget * parent,QNetworkAccessManager * networkManager)65 EmbeddedWebView::EmbeddedWebView(QWidget *parent, QNetworkAccessManager *networkManager)
66     : QWebView(parent)
67     , m_scrollParent(nullptr)
68     , m_resizeInProgress(0)
69     , m_staticWidth(0)
70     , m_colorScheme(ColorScheme::System)
71 {
72     // set to expanding, ie. "freely" - this is important so the widget will attempt to shrink below the sizehint!
73     setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
74     setFocusPolicy(Qt::StrongFocus); // not by the wheel
75     setPage(new ErrorCheckingPage(this));
76     page()->setNetworkAccessManager(networkManager);
77 
78     QWebSettings *s = settings();
79     s->setAttribute(QWebSettings::JavascriptEnabled, false);
80     s->setAttribute(QWebSettings::JavaEnabled, false);
81     s->setAttribute(QWebSettings::PluginsEnabled, false);
82     s->setAttribute(QWebSettings::PrivateBrowsingEnabled, true);
83     s->setAttribute(QWebSettings::JavaEnabled, false);
84     s->setAttribute(QWebSettings::OfflineStorageDatabaseEnabled, false);
85     s->setAttribute(QWebSettings::OfflineWebApplicationCacheEnabled, false);
86     s->setAttribute(QWebSettings::LocalStorageDatabaseEnabled, false);
87     s->clearMemoryCaches();
88 
89     page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
90     connect(this, &QWebView::linkClicked, this, &EmbeddedWebView::slotLinkClicked);
91     connect(this, &QWebView::loadFinished, this, &EmbeddedWebView::handlePageLoadFinished);
92     connect(page()->mainFrame(), &QWebFrame::contentsSizeChanged, this, &EmbeddedWebView::handlePageLoadFinished);
93 
94     // Scrolling is implemented on upper layers
95     page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);
96     page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);
97 
98     // Setup shortcuts for standard actions
99     QAction *copyAction = page()->action(QWebPage::Copy);
100     copyAction->setShortcut(tr("Ctrl+C"));
101     addAction(copyAction);
102 
103     m_autoScrollTimer = new QTimer(this);
104     m_autoScrollTimer->setInterval(50);
105     connect(m_autoScrollTimer, &QTimer::timeout, this, &EmbeddedWebView::autoScroll);
106 
107     m_sizeContrainTimer = new QTimer(this);
108     m_sizeContrainTimer->setInterval(50);
109     m_sizeContrainTimer->setSingleShot(true);
110     connect(m_sizeContrainTimer, &QTimer::timeout, this, &EmbeddedWebView::constrainSize);
111 
112     setContextMenuPolicy(Qt::NoContextMenu);
113     findScrollParent();
114 
115     addCustomStylesheet(QString());
116 }
117 
constrainSize()118 void EmbeddedWebView::constrainSize()
119 {
120     Incrementor dummy(&m_resizeInProgress);
121 
122     if (!(m_scrollParent && page() && page()->mainFrame()))
123         return; // should not happen but who knows
124 
125     // Prevent expensive operation where a resize triggers one extra resizing operation.
126     // This is very visible on large attachments, and in fact could possibly lead to recursion as the
127     // contentsSizeChanged signal is connected to handlePageLoadFinished.
128     if (m_resizeInProgress > 1)
129         return;
130 
131     // the m_scrollParentPadding measures the summed up horizontal paddings of this view compared to
132     // its m_scrollParent
133     setMinimumSize(0,0);
134     setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
135     if (m_staticWidth) {
136         resize(m_staticWidth, QWIDGETSIZE_MAX - 1);
137         page()->setViewportSize(QSize(m_staticWidth, 32));
138     } else {
139         // resize so that the viewport has much vertical and wanted horizontal space
140         resize(m_scrollParent->width() - m_scrollParentPadding, QWIDGETSIZE_MAX);
141         // resize the PAGES viewport to this width and a minimum height
142         page()->setViewportSize(QSize(m_scrollParent->width() - m_scrollParentPadding, 32));
143     }
144     // now the page has an idea about it's demanded size
145     const QSize bestSize = page()->mainFrame()->contentsSize();
146     // set the viewport to that size! - Otherwise it'd still be our "suggestion"
147     page()->setViewportSize(bestSize);
148     // fix the widgets size so the layout doesn't have much choice
149     setFixedSize(bestSize);
150     m_sizeContrainTimer->stop(); // we caused spurious resize events
151 }
152 
slotLinkClicked(const QUrl & url)153 void EmbeddedWebView::slotLinkClicked(const QUrl &url)
154 {
155     // Only allow external http:// and https:// links for safety reasons
156     if (url.scheme().toLower() == QLatin1String("http") || url.scheme().toLower() == QLatin1String("https")) {
157         QDesktopServices::openUrl(url);
158     } else if (url.scheme().toLower() == QLatin1String("mailto")) {
159         // The mailto: scheme is registered by Gui::MainWindow and handled internally;
160         // even if it wasn't, opening a third-party application in response to a
161         // user-initiated click does not pose a security risk
162         QUrl betterUrl(url);
163         betterUrl.setScheme(url.scheme().toLower());
164         QDesktopServices::openUrl(betterUrl);
165     }
166 }
167 
handlePageLoadFinished()168 void EmbeddedWebView::handlePageLoadFinished()
169 {
170     constrainSize();
171 
172     // We've already set in in our constructor, but apparently it isn't enough (Qt 4.8.0 on X11).
173     // Let's do it again here, it works.
174     Qt::ScrollBarPolicy policy = isWindow() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff;
175     page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, policy);
176     page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, policy);
177     page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
178 }
179 
changeEvent(QEvent * e)180 void EmbeddedWebView::changeEvent(QEvent *e)
181 {
182     QWebView::changeEvent(e);
183     if (e->type() == QEvent::ParentChange)
184         findScrollParent();
185 }
186 
eventFilter(QObject * o,QEvent * e)187 bool EmbeddedWebView::eventFilter(QObject *o, QEvent *e)
188 {
189     if (o == m_scrollParent) {
190         if (e->type() == QEvent::Resize) {
191             if (!m_staticWidth)
192                 m_sizeContrainTimer->start();
193         } else if (e->type() == QEvent::Enter) {
194             m_autoScrollPixels = 0;
195             m_autoScrollTimer->stop();
196         }
197     }
198     return QWebView::eventFilter(o, e);
199 }
200 
autoScroll()201 void EmbeddedWebView::autoScroll()
202 {
203     if (!(m_scrollParent && m_autoScrollPixels)) {
204         m_autoScrollPixels = 0;
205         m_autoScrollTimer->stop();
206         return;
207     }
208     if (QScrollBar *bar = static_cast<QAbstractScrollArea*>(m_scrollParent)->verticalScrollBar()) {
209         bar->setValue(bar->value() + m_autoScrollPixels);
210     }
211 }
212 
mouseMoveEvent(QMouseEvent * e)213 void EmbeddedWebView::mouseMoveEvent(QMouseEvent *e)
214 {
215     if ((e->buttons() & Qt::LeftButton) && m_scrollParent) {
216         m_autoScrollPixels = 0;
217         const QPoint pos = mapTo(m_scrollParent, e->pos());
218         if (pos.y() < 0)
219             m_autoScrollPixels = pos.y();
220         else if (pos.y() > m_scrollParent->rect().height())
221             m_autoScrollPixels = pos.y() - m_scrollParent->rect().height();
222         autoScroll();
223         m_autoScrollTimer->start();
224     }
225     QWebView::mouseMoveEvent(e);
226 }
227 
mouseReleaseEvent(QMouseEvent * e)228 void EmbeddedWebView::mouseReleaseEvent(QMouseEvent *e)
229 {
230     if (!(e->buttons() & Qt::LeftButton)) {
231         m_autoScrollPixels = 0;
232         m_autoScrollTimer->stop();
233     }
234     QWebView::mouseReleaseEvent(e);
235 }
236 
findScrollParent()237 void EmbeddedWebView::findScrollParent()
238 {
239     if (m_scrollParent)
240         m_scrollParent->removeEventFilter(this);
241     m_scrollParent = 0;
242     const int frameWidth = 2*style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
243     m_scrollParentPadding = frameWidth;
244     QWidget *runner = this;
245     int left, top, right, bottom;
246     while (runner) {
247         runner->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
248         runner->getContentsMargins(&left, &top, &right, &bottom);
249         m_scrollParentPadding += left + right + frameWidth;
250         if (runner->layout()) {
251             runner->layout()->getContentsMargins(&left, &top, &right, &bottom);
252             m_scrollParentPadding += left + right;
253         }
254         QWidget *p = runner->parentWidget();
255         if (p && qobject_cast<MessageView*>(runner) && // is this a MessageView?
256             p->objectName() == QLatin1String("qt_scrollarea_viewport") && // in a viewport?
257             qobject_cast<QAbstractScrollArea*>(p->parentWidget())) { // that is used?
258             p->getContentsMargins(&left, &top, &right, &bottom);
259             m_scrollParentPadding += left + right + frameWidth;
260             if (p->layout()) {
261                 p->layout()->getContentsMargins(&left, &top, &right, &bottom);
262                 m_scrollParentPadding += left + right;
263             }
264             m_scrollParent = p->parentWidget();
265             break; // then we have our actual message view
266         }
267         runner = p;
268     }
269     m_scrollParentPadding += style()->pixelMetric(QStyle::PM_ScrollBarExtent, 0, m_scrollParent);
270     if (m_scrollParent)
271         m_scrollParent->installEventFilter(this);
272 }
273 
showEvent(QShowEvent * se)274 void EmbeddedWebView::showEvent(QShowEvent *se)
275 {
276     QWebView::showEvent(se);
277     Qt::ScrollBarPolicy policy = isWindow() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff;
278     page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAsNeeded);
279     page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, policy);
280     if (isWindow()) {
281         resize(640,480);
282     } else if (!m_scrollParent) // it would be much easier if the parents were just passed with the constructor ;-)
283         findScrollParent();
284 }
285 
sizeHint() const286 QSize EmbeddedWebView::sizeHint() const
287 {
288     return QSize(32,32); // QWebView returns 800x600 what will lead to too wide pages for our implementation
289 }
290 
scrollParent() const291 QWidget *EmbeddedWebView::scrollParent() const
292 {
293     return m_scrollParent;
294 }
295 
setStaticWidth(int staticWidth)296 void EmbeddedWebView::setStaticWidth(int staticWidth)
297 {
298     m_staticWidth = staticWidth;
299 }
300 
staticWidth() const301 int EmbeddedWebView::staticWidth() const
302 {
303     return m_staticWidth;
304 }
305 
ErrorCheckingPage(QObject * parent)306 ErrorCheckingPage::ErrorCheckingPage(QObject *parent): QWebPage(parent)
307 {
308 }
309 
supportsExtension(Extension extension) const310 bool ErrorCheckingPage::supportsExtension(Extension extension) const
311 {
312     if (extension == ErrorPageExtension)
313         return true;
314     else
315         return false;
316 }
317 
extension(Extension extension,const ExtensionOption * option,ExtensionReturn * output)318 bool ErrorCheckingPage::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output)
319 {
320     if (extension != ErrorPageExtension)
321         return false;
322 
323     const ErrorPageExtensionOption *input = static_cast<const ErrorPageExtensionOption *>(option);
324     ErrorPageExtensionReturn *res = static_cast<ErrorPageExtensionReturn *>(output);
325     if (input && res) {
326         if (input->url.scheme() == QLatin1String("trojita-imap")) {
327             if (input->domain == QtNetwork && input->error == QNetworkReply::TimeoutError) {
328                 res->content = tr("<img src=\"%2\"/><span style=\"font-family: sans-serif; color: gray\">"
329                                   "Uncached data not available when offline</span>")
330                         .arg(Util::resizedImageAsDataUrl(QStringLiteral(":/icons/network-offline.svg"), 32)).toUtf8();
331                 return true;
332             }
333         }
334         res->content = input->errorString.toUtf8();
335         res->contentType = QStringLiteral("text/plain");
336     }
337     return true;
338 }
339 
supportedColorSchemes() const340 std::map<EmbeddedWebView::ColorScheme, QString> EmbeddedWebView::supportedColorSchemes() const
341 {
342     std::map<EmbeddedWebView::ColorScheme, QString> map;
343     map[ColorScheme::System] = tr("System colors");
344     map[ColorScheme::AdjustedSystem] = tr("System theme adjusted for better contrast");
345     map[ColorScheme::BlackOnWhite] = tr("Black on white, forced");
346     return map;
347 }
348 
setColorScheme(const ColorScheme colorScheme)349 void EmbeddedWebView::setColorScheme(const ColorScheme colorScheme)
350 {
351     m_colorScheme = colorScheme;
352     addCustomStylesheet(m_customCss);
353 }
354 
addCustomStylesheet(const QString & css)355 void EmbeddedWebView::addCustomStylesheet(const QString &css)
356 {
357     m_customCss = css;
358 
359     QWebSettings *s = settings();
360     QString bgName, fgName;
361     QColor bg = palette().color(QPalette::Active, QPalette::Base),
362            fg = palette().color(QPalette::Active, QPalette::Text);
363 
364     switch (m_colorScheme) {
365     case ColorScheme::BlackOnWhite:
366         bgName = QStringLiteral("white !important");
367         fgName = QStringLiteral("black !important");
368         break;
369     case ColorScheme::AdjustedSystem:
370     {
371         // This is HTML, and the authors of that markup are free to specify only the background colors, or only the foreground colors.
372         // No matter what we pass from outside, there will always be some color which will result in unreadable text, and we can do
373         // nothing except adding !important everywhere to fix this.
374         // This code attempts to create a color which will try to produce exactly ugly results for both dark-on-bright and
375         // bright-on-dark segments of text. However, it's pure alchemy and only a limited heuristics. If you do not like this, please
376         // submit patches (or talk to the HTML producers, hehehe).
377         const int v = bg.value();
378         if (v < 96 && fg.value() > 128 + v/2) {
379             int h,s,vv,a;
380             fg.getHsv(&h, &s, &vv, &a) ;
381             fg.setHsv(h, s, 128+v/2, a);
382         }
383         bgName = bg.name();
384         fgName = fg.name();
385         break;
386     }
387     case ColorScheme::System:
388         bgName = bg.name();
389         fgName = fg.name();
390         break;
391     }
392 
393 
394     const QString urlPrefix(QStringLiteral("data:text/css;charset=utf-8;base64,"));
395     const QString myColors(QStringLiteral("body { background-color: %1; color: %2; }\n").arg(bgName, fgName));
396     s->setUserStyleSheetUrl(QString::fromUtf8(urlPrefix.toUtf8() + (myColors + m_customCss).toUtf8().toBase64()));
397 }
398 
399 }
400