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