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"
27 #include <QAbstractScrollArea>
29 #include <QApplication>
30 #include <QDesktopServices>
31 #include <QDesktopWidget>
33 #include <QMouseEvent>
34 #include <QNetworkReply>
37 #include <QStyleFactory>
40 #include <QWebHistory>
46 /** @short RAII pattern for counter manipulation */
50 Incrementor(int *what
): m_int(what
)
57 Q_ASSERT(*m_int
>= 0);
66 EmbeddedWebView::EmbeddedWebView(QWidget
*parent
, QNetworkAccessManager
*networkManager
, QSettings
*profileSettings
)
68 , m_scrollParent(nullptr)
69 , m_resizeInProgress(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
);
116 addCustomStylesheet(QString());
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)
134 // the m_scrollParentPadding measures the summed up horizontal paddings of this view compared to
135 // its m_scrollParent
137 setMaximumSize(QWIDGETSIZE_MAX
, QWIDGETSIZE_MAX
);
139 resize(m_staticWidth
, QWIDGETSIZE_MAX
- 1);
140 page()->setViewportSize(QSize(m_staticWidth
, 32));
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
166 betterUrl
.setScheme(url
.scheme().toLower());
167 QDesktopServices::openUrl(betterUrl
);
171 void EmbeddedWebView::handlePageLoadFinished()
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
)
191 bool EmbeddedWebView::eventFilter(QObject
*o
, QEvent
*e
)
193 if (o
== m_scrollParent
) {
194 if (e
->type() == QEvent::Resize
) {
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();
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());
223 m_autoScrollPixels
= pos
.y();
224 else if (pos
.y() > m_scrollParent
->rect().height())
225 m_autoScrollPixels
= pos
.y() - m_scrollParent
->rect().height();
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()
244 m_scrollParent
->removeEventFilter(this);
246 const int frameWidth
= 2*style()->pixelMetric(QStyle::PM_DefaultFrameWidth
);
247 m_scrollParentPadding
= frameWidth
;
248 QWidget
*runner
= this;
249 int left
, top
, right
, bottom
;
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
;
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
273 m_scrollParentPadding
+= style()->pixelMetric(QStyle::PM_ScrollBarExtent
, 0, 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
);
286 } else if (!m_scrollParent
) // it would be much easier if the parents were just passed with the constructor ;-)
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
)
322 bool ErrorCheckingPage::extension(Extension extension
, const ExtensionOption
*option
, ExtensionReturn
*output
)
324 if (extension
!= ErrorPageExtension
)
327 const ErrorPageExtensionOption
*input
= static_cast<const ErrorPageExtensionOption
*>(option
);
328 ErrorPageExtensionReturn
*res
= static_cast<ErrorPageExtensionReturn
*>(output
);
330 if (input
->url
.scheme() == QLatin1String("trojita-imap")) {
332 if (input
->domain
== QtNetwork
) {
333 switch (input
->error
) {
334 case QNetworkReply::TimeoutError
:
335 emblem
= QStringLiteral("network-disconnect");
337 case QNetworkReply::ContentNotFoundError
:
338 emblem
= QStringLiteral("emblem-error");
340 case QNetworkReply::UnknownProxyError
:
341 emblem
= QStringLiteral("emblem-error");
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();
353 res
->content
= input
->errorString
.toUtf8();
354 res
->contentType
= QStringLiteral("text/plain");
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");
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
)
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");
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) {
417 fg
.getHsv(&h
, &s
, &vv
, &a
) ;
418 fg
.setHsv(h
, s
, 128+v
/2, a
);
424 case ColorScheme::System
:
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()));