Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qlitehtmlwidget.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
5
7
8#include <QDebug>
9#include <QPaintEvent>
10#include <QPainter>
11#include <QScrollBar>
12#include <QStyle>
13
14const int kScrollBarStep = 40;
15
16// copied from include/litehtml/master_css.h
17const char master_css[] = R"##(
18html {
19 display: block;
20 position: relative;
21}
22
23head {
24 display: none
25}
26
27meta {
28 display: none
29}
30
31title {
32 display: none
33}
34
35link {
36 display: none
37}
38
39style {
40 display: none
41}
42
43script {
44 display: none
45}
46
47body {
48 display:block;
49 margin:8px;
50}
51
52p {
53 display:block;
54 margin-top:1em;
55 margin-bottom:1em;
56}
57
58b, strong {
59 display:inline;
60 font-weight:bold;
61}
62
63i, em, cite {
64 display:inline;
65 font-style:italic;
66}
67
68ins, u {
69 text-decoration:underline
70}
71
72del, s, strike {
73 text-decoration:line-through
74}
75
76center
77{
78 text-align:center;
79 display:block;
80}
81
82a:link
83{
84 text-decoration: underline;
85 color: #00f;
86 cursor: pointer;
87}
88
89h1, h2, h3, h4, h5, h6, div {
90 display:block;
91}
92
93h1 {
94 font-weight:bold;
95 margin-top:0.67em;
96 margin-bottom:0.67em;
97 font-size: 2em;
98}
99
100h2 {
101 font-weight:bold;
102 margin-top:0.83em;
103 margin-bottom:0.83em;
104 font-size: 1.5em;
105}
106
107h3 {
108 font-weight:bold;
109 margin-top:1em;
110 margin-bottom:1em;
111 font-size:1.17em;
112}
113
114h4 {
115 font-weight:bold;
116 margin-top:1.33em;
117 margin-bottom:1.33em
118}
119
120h5 {
121 font-weight:bold;
122 margin-top:1.67em;
123 margin-bottom:1.67em;
124 font-size:.83em;
125}
126
127h6 {
128 font-weight:bold;
129 margin-top:2.33em;
130 margin-bottom:2.33em;
131 font-size:.67em;
132}
133
134br {
135 display:inline-block;
136}
137
138br[clear="all"]
139{
140 clear:both;
141}
142
143br[clear="left"]
144{
145 clear:left;
146}
147
148br[clear="right"]
149{
150 clear:right;
151}
152
153span {
154 display:inline
155}
156
157img {
158 display: inline-block;
159}
160
161img[align="right"]
162{
163 float: right;
164}
165
166img[align="left"]
167{
168 float: left;
169}
170
171hr {
172 display: block;
173 margin-top: 0.5em;
174 margin-bottom: 0.5em;
175 margin-left: auto;
176 margin-right: auto;
177 border-style: inset;
178 border-width: 1px
179}
180
181
182/***************** TABLES ********************/
183
184table {
185 display: table;
186 border-collapse: separate;
187 border-spacing: 2px;
188 border-top-color:gray;
189 border-left-color:gray;
190 border-bottom-color:black;
191 border-right-color:black;
192 font-size: medium;
193 font-weight: normal;
194 font-style: normal;
195}
196
197tbody, tfoot, thead {
198 display:table-row-group;
199 vertical-align:middle;
200}
201
202tr {
203 display: table-row;
204 vertical-align: inherit;
205 border-color: inherit;
206}
207
208td, th {
209 display: table-cell;
210 vertical-align: inherit;
211 border-width:1px;
212 padding:1px;
213}
214
215th {
216 font-weight: bold;
217}
218
219table[border] {
220 border-style:solid;
221}
222
223table[border|=0] {
224 border-style:none;
225}
226
227table[border] td, table[border] th {
228 border-style:solid;
229 border-top-color:black;
230 border-left-color:black;
231 border-bottom-color:gray;
232 border-right-color:gray;
233}
234
235table[border|=0] td, table[border|=0] th {
236 border-style:none;
237}
238
239table[align=left] {
240 float: left;
241}
242
243table[align=right] {
244 float: right;
245}
246
247table[align=center] {
248 margin-left: auto;
249 margin-right: auto;
250}
251
252caption {
253 display: table-caption;
254}
255
256td[nowrap], th[nowrap] {
257 white-space:nowrap;
258}
259
260tt, code, kbd, samp {
261 font-family: monospace
262}
263
264pre, xmp, plaintext, listing {
265 display: block;
266 font-family: monospace;
267 white-space: pre;
268 margin: 1em 0
269}
270
271/***************** LISTS ********************/
272
273ul, menu, dir {
274 display: block;
275 list-style-type: disc;
276 margin-top: 1em;
277 margin-bottom: 1em;
278 margin-left: 0;
279 margin-right: 0;
280 padding-left: 40px
281}
282
283ol {
284 display: block;
285 list-style-type: decimal;
286 margin-top: 1em;
287 margin-bottom: 1em;
288 margin-left: 0;
289 margin-right: 0;
290 padding-left: 40px
291}
292
293li {
294 display: list-item;
295}
296
297ul ul, ol ul {
298 list-style-type: circle;
299}
300
301ol ol ul, ol ul ul, ul ol ul, ul ul ul {
302 list-style-type: square;
303}
304
305dd {
306 display: block;
307 margin-left: 40px;
308}
309
310dl {
311 display: block;
312 margin-top: 1em;
313 margin-bottom: 1em;
314 margin-left: 0;
315 margin-right: 0;
316}
317
318dt {
319 display: block;
320}
321
322ol ul, ul ol, ul ul, ol ol {
323 margin-top: 0;
324 margin-bottom: 0
325}
326
327blockquote {
328 display: block;
329 margin-top: 1em;
330 margin-bottom: 1em;
331 margin-left: 40px;
332 margin-right: 40px;
333}
334
335/*********** FORM ELEMENTS ************/
336
337form {
338 display: block;
339 margin-top: 0em;
340}
341
342option {
343 display: none;
344}
345
346input, textarea, keygen, select, button, isindex {
347 margin: 0em;
348 color: initial;
349 line-height: normal;
350 text-transform: none;
351 text-indent: 0;
352 text-shadow: none;
353 display: inline-block;
354}
355input[type="hidden"] {
356 display: none;
357}
358
359
360article, aside, footer, header, hgroup, nav, section
361{
362 display: block;
363}
364
365sub {
366 vertical-align: sub;
367 font-size: smaller;
368}
369
370sup {
371 vertical-align: super;
372 font-size: smaller;
373}
374
375figure {
376 display: block;
377 margin-top: 1em;
378 margin-bottom: 1em;
379 margin-left: 40px;
380 margin-right: 40px;
381}
382
383figcaption {
384 display: block;
385}
386
387)##";
388
399
400QLiteHtmlWidget::QLiteHtmlWidget(QWidget *parent)
401 : QAbstractScrollArea(parent)
402 , d(new QLiteHtmlWidgetPrivate)
403{
404 setMouseTracking(true);
405 horizontalScrollBar()->setSingleStep(kScrollBarStep);
406 verticalScrollBar()->setSingleStep(kScrollBarStep);
407
408 d->documentContainer.setCursorCallback([this](const QCursor &c) { viewport()->setCursor(c); });
409 d->documentContainer.setPaletteCallback([this] { return palette(); });
410 d->documentContainer.setLinkCallback([this](const QUrl &url) {
411 QUrl fullUrl = url;
412 if (url.isRelative() && url.path(QUrl::FullyEncoded).isEmpty()) { // fragment/anchor only
413 fullUrl = d->url;
414 fullUrl.setFragment(url.fragment(QUrl::FullyEncoded));
415 }
416 // delay because document may not be changed directly during this callback
417 QMetaObject::invokeMethod(this, [this, fullUrl] { emit linkClicked(fullUrl); },
418 Qt::QueuedConnection);
419 });
420 d->documentContainer.setClipboardCallback([this](bool yes) { emit copyAvailable(yes); });
421
422 // TODO adapt mastercss to palette (default text & background color)
423 d->context.setMasterStyleSheet(master_css);
424}
425
426QLiteHtmlWidget::~QLiteHtmlWidget()
427{
428 delete d;
429}
430
431void QLiteHtmlWidget::setUrl(const QUrl &url)
432{
433 d->url = url;
434 QUrl baseUrl = url;
435 baseUrl.setFragment({});
436 const QString path = baseUrl.path(QUrl::FullyEncoded);
437 const int lastSlash = path.lastIndexOf('/');
438 const QString basePath = lastSlash >= 0 ? path.left(lastSlash) : QString();
439 baseUrl.setPath(basePath);
440 d->documentContainer.setBaseUrl(baseUrl.toString(QUrl::FullyEncoded));
441 QMetaObject::invokeMethod(this, [this] { updateHightlightedLink(); },
442 Qt::QueuedConnection);
443}
444
445QUrl QLiteHtmlWidget::url() const
446{
447 return d->url;
448}
449
450void QLiteHtmlWidget::setHtml(const QString &content)
451{
452 d->html = content;
453 d->documentContainer.setPaintDevice(viewport());
454 d->documentContainer.setDocument(content.toUtf8(), &d->context);
455 verticalScrollBar()->setValue(0);
456 horizontalScrollBar()->setValue(0);
457 render();
458 QMetaObject::invokeMethod(this, [this] { updateHightlightedLink(); },
459 Qt::QueuedConnection);
460}
461
462QString QLiteHtmlWidget::html() const
463{
464 return d->html;
465}
466
467QString QLiteHtmlWidget::title() const
468{
469 return d->documentContainer.caption();
470}
471
472void QLiteHtmlWidget::setZoomFactor(qreal scale)
473{
474 Q_ASSERT(scale != 0);
475 d->zoomFactor = scale;
476 withFixedTextPosition([this] { render(); });
477}
478
479qreal QLiteHtmlWidget::zoomFactor() const
480{
481 return d->zoomFactor;
482}
483
484bool QLiteHtmlWidget::findText(const QString &text,
485 QTextDocument::FindFlags flags,
486 bool incremental,
487 bool *wrapped)
488{
489 bool success = false;
490 QVector<QRect> oldSelection;
491 QVector<QRect> newSelection;
492 d->documentContainer
493 .findText(text, flags, incremental, wrapped, &success, &oldSelection, &newSelection);
494 // scroll to search result position and/or redraw as necessary
495 QRect newSelectionCombined;
496 for (const QRect &r : std::as_const(newSelection))
497 newSelectionCombined = newSelectionCombined.united(r);
498 QScrollBar *vBar = verticalScrollBar();
499 const int top = newSelectionCombined.top();
500 const int bottom = newSelectionCombined.bottom() - toVirtual(viewport()->size()).height();
501 if (success && top < vBar->value() && vBar->minimum() <= top) {
502 vBar->setValue(top);
503 } else if (success && vBar->value() < bottom && bottom <= vBar->maximum()) {
504 vBar->setValue(bottom);
505 } else {
506 viewport()->update(fromVirtual(newSelectionCombined.translated(-scrollPosition())));
507 for (const QRect &r : std::as_const(oldSelection))
508 viewport()->update(fromVirtual(r.translated(-scrollPosition())));
509 }
510 return success;
511}
512
513void QLiteHtmlWidget::setDefaultFont(const QFont &font)
514{
515 withFixedTextPosition([this, &font] {
516 d->documentContainer.setDefaultFont(font);
517 render();
518 });
519}
520
521QFont QLiteHtmlWidget::defaultFont() const
522{
523 return d->documentContainer.defaultFont();
524}
525
526void QLiteHtmlWidget::setAntialias(bool on)
527{
528 withFixedTextPosition([this, on] {
529 d->documentContainer.setAntialias(on);
530 // force litehtml to recreate fonts
531 setHtml(d->html);
532 });
533}
534
535void QLiteHtmlWidget::scrollToAnchor(const QString &name)
536{
537 if (!d->documentContainer.hasDocument())
538 return;
539 horizontalScrollBar()->setValue(0);
540 if (name.isEmpty()) {
541 verticalScrollBar()->setValue(0);
542 return;
543 }
544 const int y = d->documentContainer.anchorY(name);
545 if (y >= 0)
546 verticalScrollBar()->setValue(std::min(y, verticalScrollBar()->maximum()));
547}
548
549void QLiteHtmlWidget::setResourceHandler(const QLiteHtmlWidget::ResourceHandler &handler)
550{
551 d->documentContainer.setDataCallback(handler);
552}
553
554QString QLiteHtmlWidget::selectedText() const
555{
556 return d->documentContainer.selectedText();
557}
558
559void QLiteHtmlWidget::paintEvent(QPaintEvent *event)
560{
561 if (!d->documentContainer.hasDocument())
562 return;
563 d->documentContainer.setScrollPosition(scrollPosition());
564 QPainter p(viewport());
565 p.setWorldTransform(QTransform().scale(d->zoomFactor, d->zoomFactor));
566 p.setRenderHint(QPainter::SmoothPixmapTransform, true);
567 p.setRenderHint(QPainter::Antialiasing, true);
568 d->documentContainer.draw(&p, toVirtual(event->rect()));
569}
570
571void QLiteHtmlWidget::resizeEvent(QResizeEvent *event)
572{
573 withFixedTextPosition([this, event] {
574 QAbstractScrollArea::resizeEvent(event);
575 render();
576 });
577}
578
579void QLiteHtmlWidget::mouseMoveEvent(QMouseEvent *event)
580{
581 QPoint viewportPos;
582 QPoint pos;
583 htmlPos(event->pos(), &viewportPos, &pos);
584 const QVector<QRect> areas = d->documentContainer.mouseMoveEvent(pos, viewportPos);
585 for (const QRect &r : areas)
586 viewport()->update(fromVirtual(r.translated(-scrollPosition())));
587
588 updateHightlightedLink();
589}
590
591void QLiteHtmlWidget::mousePressEvent(QMouseEvent *event)
592{
593 QPoint viewportPos;
594 QPoint pos;
595 htmlPos(event->pos(), &viewportPos, &pos);
596 const QVector<QRect> areas = d->documentContainer.mousePressEvent(pos, viewportPos, event->button());
597 for (const QRect &r : areas)
598 viewport()->update(fromVirtual(r.translated(-scrollPosition())));
599}
600
601void QLiteHtmlWidget::mouseReleaseEvent(QMouseEvent *event)
602{
603 QPoint viewportPos;
604 QPoint pos;
605 htmlPos(event->pos(), &viewportPos, &pos);
606 const QVector<QRect> areas = d->documentContainer.mouseReleaseEvent(pos, viewportPos, event->button());
607 for (const QRect &r : areas)
608 viewport()->update(fromVirtual(r.translated(-scrollPosition())));
609}
610
611void QLiteHtmlWidget::mouseDoubleClickEvent(QMouseEvent *event)
612{
613 QPoint viewportPos;
614 QPoint pos;
615 htmlPos(event->pos(), &viewportPos, &pos);
616 const QVector<QRect> areas = d->documentContainer.mouseDoubleClickEvent(pos, viewportPos, event->button());
617 for (const QRect &r : areas) {
618 viewport()->update(fromVirtual(r.translated(-scrollPosition())));
619 }
620}
621
622void QLiteHtmlWidget::leaveEvent(QEvent *event)
623{
624 Q_UNUSED(event)
625 const QVector<QRect> areas = d->documentContainer.leaveEvent();
626 for (const QRect &r : areas)
627 viewport()->update(fromVirtual(r.translated(-scrollPosition())));
628 setHightlightedLink(QUrl());
629}
630
631void QLiteHtmlWidget::contextMenuEvent(QContextMenuEvent *event)
632{
633 QPoint viewportPos;
634 QPoint pos;
635 htmlPos(event->pos(), &viewportPos, &pos);
636 emit contextMenuRequested(event->pos(), d->documentContainer.linkAt(pos, viewportPos));
637}
638
640{
641 if (key == Qt::Key_Home)
642 return QAbstractSlider::SliderToMinimum;
643 if (key == Qt::Key_End)
644 return QAbstractSlider::SliderToMaximum;
645 if (key == Qt::Key_PageUp)
646 return QAbstractSlider::SliderPageStepSub;
647 if (key == Qt::Key_PageDown)
648 return QAbstractSlider::SliderPageStepAdd;
649 return QAbstractSlider::SliderNoAction;
650}
651
652void QLiteHtmlWidget::keyPressEvent(QKeyEvent *event)
653{
654 if (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::KeypadModifier) {
655 const QAbstractSlider::SliderAction sliderAction = getSliderAction(event->key());
656 if (sliderAction != QAbstractSlider::SliderNoAction) {
657 verticalScrollBar()->triggerAction(sliderAction);
658 event->accept();
659 return;
660 }
661 }
662
663 QAbstractScrollArea::keyPressEvent(event);
664}
665
666void QLiteHtmlWidget::updateHightlightedLink()
667{
668 QPoint viewportPos;
669 QPoint pos;
670 htmlPos(mapFromGlobal(QCursor::pos()), &viewportPos, &pos);
671 setHightlightedLink(d->documentContainer.linkAt(pos, viewportPos));
672}
673
674void QLiteHtmlWidget::setHightlightedLink(const QUrl &url)
675{
676 if (d->lastHighlightedLink == url)
677 return;
678 d->lastHighlightedLink = url;
679 emit linkHighlighted(d->lastHighlightedLink);
680}
681
682void QLiteHtmlWidget::withFixedTextPosition(const std::function<void()> &action)
683{
684 // remember element to which to scroll after re-rendering
685 QPoint viewportPos;
686 QPoint pos;
687 htmlPos({}, &viewportPos, &pos); // top-left
688 const int y = d->documentContainer.withFixedElementPosition(pos.y(), action);
689 if (y >= 0)
690 verticalScrollBar()->setValue(std::min(y, verticalScrollBar()->maximum()));
691}
692
693void QLiteHtmlWidget::render()
694{
695 if (!d->documentContainer.hasDocument())
696 return;
697 const int fullWidth = width() / d->zoomFactor;
698 const QSize vViewportSize = toVirtual(viewport()->size());
699 const int scrollbarWidth = style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, this);
700 const int w = fullWidth - scrollbarWidth - 2;
701 d->documentContainer.render(w, vViewportSize.height());
702 // scroll bars reflect virtual/scaled size of html document
703 horizontalScrollBar()->setPageStep(vViewportSize.width());
704 horizontalScrollBar()->setRange(0, std::max(0, d->documentContainer.documentWidth() - w));
705 verticalScrollBar()->setPageStep(vViewportSize.height());
706 verticalScrollBar()
707 ->setRange(0, std::max(0, d->documentContainer.documentHeight() - vViewportSize.height()));
708 viewport()->update();
709}
710
711QPoint QLiteHtmlWidget::scrollPosition() const
712{
713 return {horizontalScrollBar()->value(), verticalScrollBar()->value()};
714}
715
716void QLiteHtmlWidget::htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const
717{
718 *viewportPos = toVirtual(viewport()->mapFromParent(pos));
719 *htmlPos = *viewportPos + scrollPosition();
720}
721
722QPoint QLiteHtmlWidget::toVirtual(const QPoint &p) const
723{
724 return {int(p.x() / d->zoomFactor), int(p.y() / d->zoomFactor)};
725}
726
727QSize QLiteHtmlWidget::toVirtual(const QSize &s) const
728{
729 return {int(s.width() / d->zoomFactor), int(s.height() / d->zoomFactor)};
730}
731
732QRect QLiteHtmlWidget::toVirtual(const QRect &r) const
733{
734 return {toVirtual(r.topLeft()), toVirtual(r.size())};
735}
736
737QRect QLiteHtmlWidget::fromVirtual(const QRect &r) const
738{
739 const QPoint tl{int(r.x() * d->zoomFactor), int(r.y() * d->zoomFactor)};
740 // round size up, and add one since the topleft point was rounded down
741 const QSize s{int(r.width() * d->zoomFactor + 0.5) + 1,
742 int(r.height() * d->zoomFactor + 0.5) + 1};
743 return {tl, s};
744}
DocumentContainerContext context
DocumentContainer documentContainer
static QAbstractSlider::SliderAction getSliderAction(int key)
const int kScrollBarStep
const char master_css[]