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