forked from qt-creator/qt-creator
If you zoom into a page (Ctrl-+) and then select, artifacts could be left behind when dragging or removing the selection again. Fiddle around with coordinate transformations (with regard to rounding) to fix that. Change-Id: I68c29d8e3559b90dbb3b93550338e483d14731bf Reviewed-by: Cristian Adam <cristian.adam@qt.io>
685 lines
16 KiB
C++
685 lines
16 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2019 The Qt Company Ltd.
|
|
** Contact: https://www.qt.io/licensing/
|
|
**
|
|
** This file is part of QLiteHtml.
|
|
**
|
|
** Commercial License Usage
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
** accordance with the commercial license agreement provided with the
|
|
** Software or, alternatively, in accordance with the terms contained in
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
|
**
|
|
** GNU General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
** General Public License version 3 as published by the Free Software
|
|
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
** included in the packaging of this file. Please review the following
|
|
** information to ensure the GNU General Public License requirements will
|
|
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include "qlitehtmlwidget.h"
|
|
|
|
#include "container_qpainter.h"
|
|
|
|
#include <QDebug>
|
|
#include <QPaintEvent>
|
|
#include <QPainter>
|
|
#include <QScrollBar>
|
|
#include <QStyle>
|
|
#include <QTimer>
|
|
|
|
#include <litehtml.h>
|
|
|
|
const int kScrollBarStep = 40;
|
|
|
|
// TODO copied from litehtml/include/master.css
|
|
const char mastercss[] = R"RAW(
|
|
html {
|
|
display: block;
|
|
height:100%;
|
|
width:100%;
|
|
position: relative;
|
|
}
|
|
|
|
head {
|
|
display: none
|
|
}
|
|
|
|
meta {
|
|
display: none
|
|
}
|
|
|
|
title {
|
|
display: none
|
|
}
|
|
|
|
link {
|
|
display: none
|
|
}
|
|
|
|
style {
|
|
display: none
|
|
}
|
|
|
|
script {
|
|
display: none
|
|
}
|
|
|
|
body {
|
|
display:block;
|
|
margin:8px;
|
|
height:100%;
|
|
width:100%;
|
|
}
|
|
|
|
p {
|
|
display:block;
|
|
margin-top:1em;
|
|
margin-bottom:1em;
|
|
}
|
|
|
|
b, strong {
|
|
display:inline;
|
|
font-weight:bold;
|
|
}
|
|
|
|
i, em {
|
|
display:inline;
|
|
font-style:italic;
|
|
}
|
|
|
|
center
|
|
{
|
|
text-align:center;
|
|
display:block;
|
|
}
|
|
|
|
a:link
|
|
{
|
|
text-decoration: underline;
|
|
color: #00f;
|
|
cursor: pointer;
|
|
}
|
|
|
|
h1, h2, h3, h4, h5, h6, div {
|
|
display:block;
|
|
}
|
|
|
|
h1 {
|
|
font-weight:bold;
|
|
margin-top:0.67em;
|
|
margin-bottom:0.67em;
|
|
font-size: 2em;
|
|
}
|
|
|
|
h2 {
|
|
font-weight:bold;
|
|
margin-top:0.83em;
|
|
margin-bottom:0.83em;
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
h3 {
|
|
font-weight:bold;
|
|
margin-top:1em;
|
|
margin-bottom:1em;
|
|
font-size:1.17em;
|
|
}
|
|
|
|
h4 {
|
|
font-weight:bold;
|
|
margin-top:1.33em;
|
|
margin-bottom:1.33em
|
|
}
|
|
|
|
h5 {
|
|
font-weight:bold;
|
|
margin-top:1.67em;
|
|
margin-bottom:1.67em;
|
|
font-size:.83em;
|
|
}
|
|
|
|
h6 {
|
|
font-weight:bold;
|
|
margin-top:2.33em;
|
|
margin-bottom:2.33em;
|
|
font-size:.67em;
|
|
}
|
|
|
|
br {
|
|
display:inline-block;
|
|
}
|
|
|
|
br[clear="all"]
|
|
{
|
|
clear:both;
|
|
}
|
|
|
|
br[clear="left"]
|
|
{
|
|
clear:left;
|
|
}
|
|
|
|
br[clear="right"]
|
|
{
|
|
clear:right;
|
|
}
|
|
|
|
span {
|
|
display:inline
|
|
}
|
|
|
|
img {
|
|
display: inline-block;
|
|
}
|
|
|
|
img[align="right"]
|
|
{
|
|
float: right;
|
|
}
|
|
|
|
img[align="left"]
|
|
{
|
|
float: left;
|
|
}
|
|
|
|
hr {
|
|
display: block;
|
|
margin-top: 0.5em;
|
|
margin-bottom: 0.5em;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
border-style: inset;
|
|
border-width: 1px
|
|
}
|
|
|
|
|
|
/***************** TABLES ********************/
|
|
|
|
table {
|
|
display: table;
|
|
border-collapse: separate;
|
|
border-spacing: 2px;
|
|
border-top-color:gray;
|
|
border-left-color:gray;
|
|
border-bottom-color:black;
|
|
border-right-color:black;
|
|
}
|
|
|
|
tbody, tfoot, thead {
|
|
display:table-row-group;
|
|
vertical-align:middle;
|
|
}
|
|
|
|
tr {
|
|
display: table-row;
|
|
vertical-align: inherit;
|
|
border-color: inherit;
|
|
}
|
|
|
|
td, th {
|
|
display: table-cell;
|
|
vertical-align: inherit;
|
|
border-width:1px;
|
|
padding:1px;
|
|
}
|
|
|
|
th {
|
|
font-weight: bold;
|
|
}
|
|
|
|
table[border] {
|
|
border-style:solid;
|
|
}
|
|
|
|
table[border|=0] {
|
|
border-style:none;
|
|
}
|
|
|
|
table[border] td, table[border] th {
|
|
border-style:solid;
|
|
border-top-color:black;
|
|
border-left-color:black;
|
|
border-bottom-color:gray;
|
|
border-right-color:gray;
|
|
}
|
|
|
|
table[border|=0] td, table[border|=0] th {
|
|
border-style:none;
|
|
}
|
|
|
|
caption {
|
|
display: table-caption;
|
|
}
|
|
|
|
td[nowrap], th[nowrap] {
|
|
white-space:nowrap;
|
|
}
|
|
|
|
tt, code, kbd, samp {
|
|
font-family: monospace
|
|
}
|
|
pre, xmp, plaintext, listing {
|
|
display: block;
|
|
font-family: monospace;
|
|
white-space: pre;
|
|
margin: 1em 0
|
|
}
|
|
|
|
/***************** LISTS ********************/
|
|
|
|
ul, menu, dir {
|
|
display: block;
|
|
list-style-type: disc;
|
|
margin-top: 1em;
|
|
margin-bottom: 1em;
|
|
margin-left: 0;
|
|
margin-right: 0;
|
|
padding-left: 40px
|
|
}
|
|
|
|
ol {
|
|
display: block;
|
|
list-style-type: decimal;
|
|
margin-top: 1em;
|
|
margin-bottom: 1em;
|
|
margin-left: 0;
|
|
margin-right: 0;
|
|
padding-left: 40px
|
|
}
|
|
|
|
li {
|
|
display: list-item;
|
|
}
|
|
|
|
ul ul, ol ul {
|
|
list-style-type: circle;
|
|
}
|
|
|
|
ol ol ul, ol ul ul, ul ol ul, ul ul ul {
|
|
list-style-type: square;
|
|
}
|
|
|
|
dd {
|
|
display: block;
|
|
margin-left: 40px;
|
|
}
|
|
|
|
dl {
|
|
display: block;
|
|
margin-top: 1em;
|
|
margin-bottom: 1em;
|
|
margin-left: 0;
|
|
margin-right: 0;
|
|
}
|
|
|
|
dt {
|
|
display: block;
|
|
}
|
|
|
|
ol ul, ul ol, ul ul, ol ol {
|
|
margin-top: 0;
|
|
margin-bottom: 0
|
|
}
|
|
|
|
blockquote {
|
|
display: block;
|
|
margin-top: 1em;
|
|
margin-bottom: 1em;
|
|
margin-left: 40px;
|
|
margin-left: 40px;
|
|
}
|
|
|
|
/*********** FORM ELEMENTS ************/
|
|
|
|
form {
|
|
display: block;
|
|
margin-top: 0em;
|
|
}
|
|
|
|
option {
|
|
display: none;
|
|
}
|
|
|
|
input, textarea, keygen, select, button, isindex {
|
|
margin: 0em;
|
|
color: initial;
|
|
line-height: normal;
|
|
text-transform: none;
|
|
text-indent: 0;
|
|
text-shadow: none;
|
|
display: inline-block;
|
|
}
|
|
input[type="hidden"] {
|
|
display: none;
|
|
}
|
|
|
|
|
|
article, aside, footer, header, hgroup, nav, section
|
|
{
|
|
display: block;
|
|
}
|
|
)RAW";
|
|
|
|
class QLiteHtmlWidgetPrivate
|
|
{
|
|
public:
|
|
litehtml::context context;
|
|
QUrl url;
|
|
DocumentContainer documentContainer;
|
|
qreal zoomFactor = 1;
|
|
};
|
|
|
|
QLiteHtmlWidget::QLiteHtmlWidget(QWidget *parent)
|
|
: QAbstractScrollArea(parent)
|
|
, d(new QLiteHtmlWidgetPrivate)
|
|
{
|
|
setMouseTracking(true);
|
|
horizontalScrollBar()->setSingleStep(kScrollBarStep);
|
|
verticalScrollBar()->setSingleStep(kScrollBarStep);
|
|
|
|
d->documentContainer.setCursorCallback([this](const QCursor &c) { viewport()->setCursor(c); });
|
|
d->documentContainer.setPaletteCallback([this] { return palette(); });
|
|
d->documentContainer.setLinkCallback([this](const QUrl &url) {
|
|
QUrl fullUrl = url;
|
|
if (url.isRelative() && url.path(QUrl::FullyEncoded).isEmpty()) { // fragment/anchor only
|
|
fullUrl = d->url;
|
|
fullUrl.setFragment(url.fragment(QUrl::FullyEncoded));
|
|
}
|
|
// delay because document may not be changed directly during this callback
|
|
QTimer::singleShot(0, this, [this, fullUrl] { emit linkClicked(fullUrl); });
|
|
});
|
|
|
|
// TODO adapt mastercss to palette (default text & background color)
|
|
d->context.load_master_stylesheet(mastercss);
|
|
}
|
|
|
|
QLiteHtmlWidget::~QLiteHtmlWidget()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
void QLiteHtmlWidget::setUrl(const QUrl &url)
|
|
{
|
|
d->url = url;
|
|
QUrl urlWithoutAnchor = url;
|
|
urlWithoutAnchor.setFragment({});
|
|
const QString urlString = urlWithoutAnchor.toString(QUrl::None);
|
|
const int lastSlash = urlString.lastIndexOf('/');
|
|
const QString baseUrl = lastSlash >= 0 ? urlString.left(lastSlash) : urlString;
|
|
d->documentContainer.set_base_url(baseUrl.toUtf8().constData());
|
|
}
|
|
|
|
QUrl QLiteHtmlWidget::url() const
|
|
{
|
|
return d->url;
|
|
}
|
|
|
|
void QLiteHtmlWidget::setHtml(const QString &content)
|
|
{
|
|
d->documentContainer.setPaintDevice(viewport());
|
|
d->documentContainer.setDocument(content.toUtf8(), &d->context);
|
|
verticalScrollBar()->setValue(0);
|
|
horizontalScrollBar()->setValue(0);
|
|
render();
|
|
}
|
|
|
|
QString QLiteHtmlWidget::title() const
|
|
{
|
|
return d->documentContainer.caption();
|
|
}
|
|
|
|
void QLiteHtmlWidget::setZoomFactor(qreal scale)
|
|
{
|
|
Q_ASSERT(scale != 0);
|
|
d->zoomFactor = scale;
|
|
withFixedTextPosition([this] { render(); });
|
|
}
|
|
|
|
qreal QLiteHtmlWidget::zoomFactor() const
|
|
{
|
|
return d->zoomFactor;
|
|
}
|
|
|
|
bool QLiteHtmlWidget::findText(const QString &text,
|
|
QTextDocument::FindFlags flags,
|
|
bool incremental,
|
|
bool *wrapped)
|
|
{
|
|
bool success = false;
|
|
QVector<QRect> oldSelection;
|
|
QVector<QRect> newSelection;
|
|
d->documentContainer
|
|
.findText(text, flags, incremental, wrapped, &success, &oldSelection, &newSelection);
|
|
// scroll to search result position and/or redraw as necessary
|
|
QRect newSelectionCombined;
|
|
for (const QRect &r : newSelection)
|
|
newSelectionCombined = newSelectionCombined.united(r);
|
|
QScrollBar *vBar = verticalScrollBar();
|
|
const int top = newSelectionCombined.top();
|
|
const int bottom = newSelectionCombined.bottom() - toVirtual(viewport()->size()).height();
|
|
if (success && top < vBar->value() && vBar->minimum() <= top) {
|
|
vBar->setValue(top);
|
|
} else if (success && vBar->value() < bottom && bottom <= vBar->maximum()) {
|
|
vBar->setValue(bottom);
|
|
} else {
|
|
viewport()->update(fromVirtual(newSelectionCombined.translated(-scrollPosition())));
|
|
for (const QRect &r : oldSelection)
|
|
viewport()->update(fromVirtual(r.translated(-scrollPosition())));
|
|
}
|
|
return success;
|
|
}
|
|
|
|
void QLiteHtmlWidget::setDefaultFont(const QFont &font)
|
|
{
|
|
d->documentContainer.setDefaultFont(font);
|
|
render();
|
|
}
|
|
|
|
QFont QLiteHtmlWidget::defaultFont() const
|
|
{
|
|
return d->documentContainer.defaultFont();
|
|
}
|
|
|
|
void QLiteHtmlWidget::scrollToAnchor(const QString &name)
|
|
{
|
|
if (!d->documentContainer.document())
|
|
return;
|
|
horizontalScrollBar()->setValue(0);
|
|
if (name.isEmpty()) {
|
|
verticalScrollBar()->setValue(0);
|
|
return;
|
|
}
|
|
litehtml::element::ptr element = d->documentContainer.document()->root()->select_one(
|
|
QString("#%1").arg(name).toStdString());
|
|
if (!element) {
|
|
element = d->documentContainer.document()->root()->select_one(
|
|
QString("[name=%1]").arg(name).toStdString());
|
|
}
|
|
if (element) {
|
|
const int y = element->get_placement().y;
|
|
verticalScrollBar()->setValue(std::min(y, verticalScrollBar()->maximum()));
|
|
}
|
|
}
|
|
|
|
void QLiteHtmlWidget::setResourceHandler(const QLiteHtmlWidget::ResourceHandler &handler)
|
|
{
|
|
d->documentContainer.setDataCallback(handler);
|
|
}
|
|
|
|
QString QLiteHtmlWidget::selectedText() const
|
|
{
|
|
return d->documentContainer.selectedText();
|
|
}
|
|
|
|
void QLiteHtmlWidget::paintEvent(QPaintEvent *event)
|
|
{
|
|
if (!d->documentContainer.document())
|
|
return;
|
|
d->documentContainer.setScrollPosition(scrollPosition());
|
|
QPainter p(viewport());
|
|
p.setWorldTransform(QTransform().scale(d->zoomFactor, d->zoomFactor));
|
|
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
p.setRenderHint(QPainter::Antialiasing, true);
|
|
d->documentContainer.draw(&p, toVirtual(event->rect()));
|
|
}
|
|
|
|
static litehtml::element::ptr elementForY(int y, const litehtml::document::ptr &document)
|
|
{
|
|
if (!document)
|
|
return {};
|
|
|
|
const std::function<litehtml::element::ptr(int, litehtml::element::ptr)> recursion =
|
|
[&recursion](int y, const litehtml::element::ptr &element) {
|
|
litehtml::element::ptr result;
|
|
const int subY = y - element->get_position().y;
|
|
if (subY <= 0)
|
|
return element;
|
|
for (int i = 0; i < int(element->get_children_count()); ++i) {
|
|
const litehtml::element::ptr child = element->get_child(i);
|
|
result = recursion(subY, child);
|
|
if (result)
|
|
return result;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return recursion(y, document->root());
|
|
}
|
|
|
|
void QLiteHtmlWidget::resizeEvent(QResizeEvent *event)
|
|
{
|
|
withFixedTextPosition([this, event] {
|
|
QAbstractScrollArea::resizeEvent(event);
|
|
render();
|
|
});
|
|
}
|
|
|
|
void QLiteHtmlWidget::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
QPoint viewportPos;
|
|
QPoint pos;
|
|
htmlPos(event->pos(), &viewportPos, &pos);
|
|
for (const QRect &r : d->documentContainer.mouseMoveEvent(pos, viewportPos))
|
|
viewport()->update(fromVirtual(r.translated(-scrollPosition())));
|
|
}
|
|
|
|
void QLiteHtmlWidget::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
QPoint viewportPos;
|
|
QPoint pos;
|
|
htmlPos(event->pos(), &viewportPos, &pos);
|
|
for (const QRect &r : d->documentContainer.mousePressEvent(pos, viewportPos, event->button()))
|
|
viewport()->update(fromVirtual(r.translated(-scrollPosition())));
|
|
}
|
|
|
|
void QLiteHtmlWidget::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
QPoint viewportPos;
|
|
QPoint pos;
|
|
htmlPos(event->pos(), &viewportPos, &pos);
|
|
for (const QRect &r : d->documentContainer.mouseReleaseEvent(pos, viewportPos, event->button()))
|
|
viewport()->update(fromVirtual(r.translated(-scrollPosition())));
|
|
}
|
|
|
|
void QLiteHtmlWidget::mouseDoubleClickEvent(QMouseEvent *event)
|
|
{
|
|
QPoint viewportPos;
|
|
QPoint pos;
|
|
htmlPos(event->pos(), &viewportPos, &pos);
|
|
for (const QRect &r :
|
|
d->documentContainer.mouseDoubleClickEvent(pos, viewportPos, event->button())) {
|
|
viewport()->update(fromVirtual(r.translated(-scrollPosition())));
|
|
}
|
|
}
|
|
|
|
void QLiteHtmlWidget::leaveEvent(QEvent *event)
|
|
{
|
|
Q_UNUSED(event)
|
|
for (const QRect &r : d->documentContainer.leaveEvent())
|
|
viewport()->update(fromVirtual(r.translated(-scrollPosition())));
|
|
}
|
|
|
|
void QLiteHtmlWidget::contextMenuEvent(QContextMenuEvent *event)
|
|
{
|
|
QPoint viewportPos;
|
|
QPoint pos;
|
|
htmlPos(event->pos(), &viewportPos, &pos);
|
|
emit contextMenuRequested(event->pos(), d->documentContainer.linkAt(pos, viewportPos));
|
|
}
|
|
|
|
void QLiteHtmlWidget::withFixedTextPosition(const std::function<void()> &action)
|
|
{
|
|
// remember element to which to scroll after re-rendering
|
|
QPoint viewportPos;
|
|
QPoint pos;
|
|
htmlPos({}, &viewportPos, &pos); // top-left
|
|
const litehtml::element::ptr element = elementForY(pos.y(), d->documentContainer.document());
|
|
action();
|
|
if (element) {
|
|
verticalScrollBar()->setValue(
|
|
std::min(element->get_placement().y, verticalScrollBar()->maximum()));
|
|
}
|
|
}
|
|
|
|
void QLiteHtmlWidget::render()
|
|
{
|
|
if (!d->documentContainer.document())
|
|
return;
|
|
const int fullWidth = width() / d->zoomFactor;
|
|
const QSize vViewportSize = toVirtual(viewport()->size());
|
|
const int scrollbarWidth = style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, this);
|
|
const int w = fullWidth - scrollbarWidth - 2;
|
|
d->documentContainer.render(w, vViewportSize.height());
|
|
// scroll bars reflect virtual/scaled size of html document
|
|
horizontalScrollBar()->setPageStep(vViewportSize.width());
|
|
horizontalScrollBar()->setRange(0, std::max(0, d->documentContainer.document()->width() - w));
|
|
verticalScrollBar()->setPageStep(vViewportSize.height());
|
|
verticalScrollBar()->setRange(0,
|
|
std::max(0,
|
|
d->documentContainer.document()->height()
|
|
- vViewportSize.height()));
|
|
viewport()->update();
|
|
}
|
|
|
|
QPoint QLiteHtmlWidget::scrollPosition() const
|
|
{
|
|
return {horizontalScrollBar()->value(), verticalScrollBar()->value()};
|
|
}
|
|
|
|
void QLiteHtmlWidget::htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const
|
|
{
|
|
*viewportPos = toVirtual(viewport()->mapFromParent(pos));
|
|
*htmlPos = *viewportPos + scrollPosition();
|
|
}
|
|
|
|
QPoint QLiteHtmlWidget::toVirtual(const QPoint &p) const
|
|
{
|
|
return {int(p.x() / d->zoomFactor), int(p.y() / d->zoomFactor)};
|
|
}
|
|
|
|
QSize QLiteHtmlWidget::toVirtual(const QSize &s) const
|
|
{
|
|
return {int(s.width() / d->zoomFactor), int(s.height() / d->zoomFactor)};
|
|
}
|
|
|
|
QRect QLiteHtmlWidget::toVirtual(const QRect &r) const
|
|
{
|
|
return {toVirtual(r.topLeft()), toVirtual(r.size())};
|
|
}
|
|
|
|
QRect QLiteHtmlWidget::fromVirtual(const QRect &r) const
|
|
{
|
|
const QPoint tl{int(r.x() * d->zoomFactor), int(r.y() * d->zoomFactor)};
|
|
// round size up, and add one since the topleft point was rounded down
|
|
const QSize s{int(r.width() * d->zoomFactor + 0.5) + 1,
|
|
int(r.height() * d->zoomFactor + 0.5) + 1};
|
|
return {tl, s};
|
|
}
|