1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qtextbrowser.h"
5#include "qtextedit_p.h"
6
7#include <qstack.h>
8#include <qapplication.h>
9#include <private/qapplication_p.h>
10#include <qevent.h>
11#include <qdebug.h>
12#include <qabstracttextdocumentlayout.h>
13#include "private/qtextdocumentlayout_p.h"
14#include <qpainter.h>
15#include <qdir.h>
16#if QT_CONFIG(whatsthis)
17#include <qwhatsthis.h>
18#endif
19#include <qtextobject.h>
20#include <qdesktopservices.h>
21#include <qstringconverter.h>
22
23QT_BEGIN_NAMESPACE
24
25using namespace Qt::StringLiterals;
26
27static inline bool shouldEnableInputMethod(QTextBrowser *texbrowser)
28{
29#if defined (Q_OS_ANDROID)
30 return !texbrowser->isReadOnly() || (texbrowser->textInteractionFlags() & Qt::TextSelectableByMouse);
31#else
32 return !texbrowser->isReadOnly();
33#endif
34}
35
36Q_LOGGING_CATEGORY(lcBrowser, "qt.text.browser")
37
38class QTextBrowserPrivate : public QTextEditPrivate
39{
40 Q_DECLARE_PUBLIC(QTextBrowser)
41public:
42 inline QTextBrowserPrivate()
43 : textOrSourceChanged(false), forceLoadOnSourceChange(false), openExternalLinks(false),
44 openLinks(true)
45#ifdef QT_KEYPAD_NAVIGATION
46 , lastKeypadScrollValue(-1)
47#endif
48 {}
49
50 void init();
51
52 struct HistoryEntry {
53 inline HistoryEntry()
54 : hpos(0), vpos(0), focusIndicatorPosition(-1),
55 focusIndicatorAnchor(-1) {}
56 QUrl url;
57 QString title;
58 int hpos;
59 int vpos;
60 int focusIndicatorPosition, focusIndicatorAnchor;
61 QTextDocument::ResourceType type = QTextDocument::UnknownResource;
62 };
63
64 HistoryEntry history(int i) const
65 {
66 if (i <= 0)
67 if (-i < stack.size())
68 return stack[stack.size()+i-1];
69 else
70 return HistoryEntry();
71 else
72 if (i <= forwardStack.size())
73 return forwardStack[forwardStack.size()-i];
74 else
75 return HistoryEntry();
76 }
77
78
79 HistoryEntry createHistoryEntry() const;
80 void restoreHistoryEntry(const HistoryEntry &entry);
81
82 QStack<HistoryEntry> stack;
83 QStack<HistoryEntry> forwardStack;
84 QUrl home;
85 QUrl currentURL;
86
87 QStringList searchPaths;
88
89 /*flag necessary to give the linkClicked() signal some meaningful
90 semantics when somebody connected to it calls setText() or
91 setSource() */
92 bool textOrSourceChanged;
93 bool forceLoadOnSourceChange;
94
95 bool openExternalLinks;
96 bool openLinks;
97
98 QTextDocument::ResourceType currentType;
99
100#ifndef QT_NO_CURSOR
101 QCursor oldCursor;
102#endif
103
104 QString findFile(const QUrl &name) const;
105
106 inline void _q_documentModified()
107 {
108 textOrSourceChanged = true;
109 forceLoadOnSourceChange = !currentURL.path().isEmpty();
110 }
111
112 void _q_activateAnchor(const QString &href);
113 void _q_highlightLink(const QString &href);
114
115 void setSource(const QUrl &url, QTextDocument::ResourceType type);
116
117 // re-imlemented from QTextEditPrivate
118 virtual QUrl resolveUrl(const QUrl &url) const override;
119 inline QUrl resolveUrl(const QString &url) const
120 { return resolveUrl(url: QUrl(url)); }
121
122#ifdef QT_KEYPAD_NAVIGATION
123 void keypadMove(bool next);
124 QTextCursor prevFocus;
125 int lastKeypadScrollValue;
126#endif
127 void emitHighlighted(const QUrl &url)
128 {
129 Q_Q(QTextBrowser);
130 emit q->highlighted(url);
131 }
132};
133Q_DECLARE_TYPEINFO(QTextBrowserPrivate::HistoryEntry, Q_RELOCATABLE_TYPE);
134
135QString QTextBrowserPrivate::findFile(const QUrl &name) const
136{
137 QString fileName;
138 if (name.scheme() == "qrc"_L1) {
139 fileName = ":/"_L1 + name.path();
140 } else if (name.scheme().isEmpty()) {
141 fileName = name.path();
142 } else {
143#if defined(Q_OS_ANDROID)
144 if (name.scheme() == "assets"_L1)
145 fileName = "assets:"_L1 + name.path();
146 else
147#endif
148 fileName = name.toLocalFile();
149 }
150
151 if (fileName.isEmpty())
152 return fileName;
153
154 if (QFileInfo(fileName).isAbsolute())
155 return fileName;
156
157 for (QString path : std::as_const(t: searchPaths)) {
158 if (!path.endsWith(c: u'/'))
159 path.append(c: u'/');
160 path.append(s: fileName);
161 if (QFileInfo(path).isReadable())
162 return path;
163 }
164
165 return fileName;
166}
167
168QUrl QTextBrowserPrivate::resolveUrl(const QUrl &url) const
169{
170 if (!url.isRelative())
171 return url;
172
173 // For the second case QUrl can merge "#someanchor" with "foo.html"
174 // correctly to "foo.html#someanchor"
175 if (!(currentURL.isRelative()
176 || (currentURL.scheme() == "file"_L1
177 && !QFileInfo(currentURL.toLocalFile()).isAbsolute()))
178 || (url.hasFragment() && url.path().isEmpty())) {
179 return currentURL.resolved(relative: url);
180 }
181
182 // this is our last resort when current url and new url are both relative
183 // we try to resolve against the current working directory in the local
184 // file system.
185 QFileInfo fi(currentURL.toLocalFile());
186 if (fi.exists()) {
187 return QUrl::fromLocalFile(localfile: fi.absolutePath() + QDir::separator()).resolved(relative: url);
188 }
189
190 return url;
191}
192
193void QTextBrowserPrivate::_q_activateAnchor(const QString &href)
194{
195 if (href.isEmpty())
196 return;
197 Q_Q(QTextBrowser);
198
199#ifndef QT_NO_CURSOR
200 viewport->setCursor(oldCursor);
201#endif
202
203 const QUrl url = resolveUrl(url: href);
204
205 if (!openLinks) {
206 emit q->anchorClicked(url);
207 return;
208 }
209
210 textOrSourceChanged = false;
211
212#ifndef QT_NO_DESKTOPSERVICES
213 bool isFileScheme =
214 url.scheme() == "file"_L1
215#if defined(Q_OS_ANDROID)
216 || url.scheme() == "assets"_L1
217#endif
218 || url.scheme() == "qrc"_L1;
219 if ((openExternalLinks && !isFileScheme && !url.isRelative())
220 || (url.isRelative() && !currentURL.isRelative() && !isFileScheme)) {
221 QDesktopServices::openUrl(url);
222 return;
223 }
224#endif
225
226 emit q->anchorClicked(url);
227
228 if (textOrSourceChanged)
229 return;
230
231 q->setSource(name: url);
232}
233
234void QTextBrowserPrivate::_q_highlightLink(const QString &anchor)
235{
236 if (anchor.isEmpty()) {
237#ifndef QT_NO_CURSOR
238 if (viewport->cursor().shape() != Qt::PointingHandCursor)
239 oldCursor = viewport->cursor();
240 viewport->setCursor(oldCursor);
241#endif
242 emitHighlighted(url: QUrl());
243 } else {
244#ifndef QT_NO_CURSOR
245 viewport->setCursor(Qt::PointingHandCursor);
246#endif
247
248 const QUrl url = resolveUrl(url: anchor);
249 emitHighlighted(url);
250 }
251}
252
253void QTextBrowserPrivate::setSource(const QUrl &url, QTextDocument::ResourceType type)
254{
255 Q_Q(QTextBrowser);
256#ifndef QT_NO_CURSOR
257 if (q->isVisible())
258 QGuiApplication::setOverrideCursor(Qt::WaitCursor);
259#endif
260 textOrSourceChanged = true;
261
262 QString txt;
263
264 bool doSetText = false;
265
266 QUrl currentUrlWithoutFragment = currentURL;
267 currentUrlWithoutFragment.setFragment(fragment: QString());
268 QUrl newUrlWithoutFragment = currentURL.resolved(relative: url);
269 newUrlWithoutFragment.setFragment(fragment: QString());
270 QString fileName = url.fileName();
271 if (type == QTextDocument::UnknownResource) {
272#if QT_CONFIG(textmarkdownreader)
273 if (fileName.endsWith(s: ".md"_L1) ||
274 fileName.endsWith(s: ".mkd"_L1) ||
275 fileName.endsWith(s: ".markdown"_L1))
276 type = QTextDocument::MarkdownResource;
277 else
278#endif
279 type = QTextDocument::HtmlResource;
280 }
281 currentType = type;
282
283 if (url.isValid()
284 && (newUrlWithoutFragment != currentUrlWithoutFragment || forceLoadOnSourceChange)) {
285 QVariant data = q->loadResource(type, name: resolveUrl(url));
286 if (data.userType() == QMetaType::QString) {
287 txt = data.toString();
288 } else if (data.userType() == QMetaType::QByteArray) {
289 QByteArray ba = data.toByteArray();
290 if (type == QTextDocument::HtmlResource) {
291 auto decoder = QStringDecoder::decoderForHtml(data: ba);
292 if (!decoder.isValid())
293 // fall back to utf8
294 decoder = QStringDecoder(QStringDecoder::Utf8);
295 txt = decoder(ba);
296 } else {
297 txt = QString::fromUtf8(ba);
298 }
299 }
300 if (Q_UNLIKELY(txt.isEmpty()))
301 qWarning(msg: "QTextBrowser: No document for %s", url.toString().toLatin1().constData());
302
303 if (q->isVisible()) {
304 const QStringView firstTag = QStringView{txt}.left(n: txt.indexOf(c: u'>') + 1);
305 if (firstTag.startsWith(s: "<qt"_L1) && firstTag.contains(s: "type"_L1) && firstTag.contains(s: "detail"_L1)) {
306#ifndef QT_NO_CURSOR
307 QGuiApplication::restoreOverrideCursor();
308#endif
309#if QT_CONFIG(whatsthis)
310 QWhatsThis::showText(pos: QCursor::pos(), text: txt, w: q);
311#endif
312 return;
313 }
314 }
315
316 currentURL = resolveUrl(url);
317 doSetText = true;
318 }
319
320 if (!home.isValid())
321 home = url;
322
323 if (doSetText) {
324 // Setting the base URL helps QTextDocument::resource() to find resources with relative paths.
325 // But don't set it unless it contains the document's path, because QTextBrowserPrivate::resolveUrl()
326 // can already deal with local files on the filesystem in case the base URL was not set.
327 QUrl baseUrl = currentURL.adjusted(options: QUrl::RemoveFilename);
328 if (!baseUrl.path().isEmpty())
329 q->document()->setBaseUrl(baseUrl);
330 q->document()->setMetaInformation(info: QTextDocument::DocumentUrl, currentURL.toString());
331 qCDebug(lcBrowser) << "loading" << currentURL << "base" << q->document()->baseUrl() << "type" << type << txt.size() << "chars";
332#if QT_CONFIG(textmarkdownreader)
333 if (type == QTextDocument::MarkdownResource)
334 q->QTextEdit::setMarkdown(txt);
335 else
336#endif
337#ifndef QT_NO_TEXTHTMLPARSER
338 q->QTextEdit::setHtml(txt);
339#else
340 q->QTextEdit::setPlainText(txt);
341#endif
342
343#ifdef QT_KEYPAD_NAVIGATION
344 prevFocus.movePosition(QTextCursor::Start);
345#endif
346 }
347
348 forceLoadOnSourceChange = false;
349
350 if (!url.fragment().isEmpty()) {
351 q->scrollToAnchor(name: url.fragment());
352 } else {
353 hbar->setValue(0);
354 vbar->setValue(0);
355 }
356#ifdef QT_KEYPAD_NAVIGATION
357 lastKeypadScrollValue = vbar->value();
358 emitHighlighted(QUrl());
359#endif
360
361#ifndef QT_NO_CURSOR
362 if (q->isVisible())
363 QGuiApplication::restoreOverrideCursor();
364#endif
365 emit q->sourceChanged(url);
366}
367
368#ifdef QT_KEYPAD_NAVIGATION
369void QTextBrowserPrivate::keypadMove(bool next)
370{
371 Q_Q(QTextBrowser);
372
373 const int height = viewport->height();
374 const int overlap = qBound(20, height / 5, 40); // XXX arbitrary, but a good balance
375 const int visibleLinkAmount = overlap; // consistent, but maybe not the best choice (?)
376 int yOffset = vbar->value();
377 int scrollYOffset = qBound(0, next ? yOffset + height - overlap : yOffset - height + overlap, vbar->maximum());
378
379 bool foundNextAnchor = false;
380 bool focusIt = false;
381 int focusedPos = -1;
382
383 QTextCursor anchorToFocus;
384
385 QRectF viewRect = QRectF(0, yOffset, control->size().width(), height);
386 QRectF newViewRect = QRectF(0, scrollYOffset, control->size().width(), height);
387 QRectF bothViewRects = viewRect.united(newViewRect);
388
389 // If we don't have a previous anchor, pretend that we had the first/last character
390 // on the screen selected.
391 if (prevFocus.isNull()) {
392 if (next)
393 prevFocus = control->cursorForPosition(QPointF(0, yOffset));
394 else
395 prevFocus = control->cursorForPosition(QPointF(control->size().width(), yOffset + height));
396 }
397
398 // First, check to see if someone has moved the scroll bars independently
399 if (lastKeypadScrollValue != yOffset) {
400 // Someone (user or programmatically) has moved us, so we might
401 // need to start looking from the current position instead of prevFocus
402
403 bool findOnScreen = true;
404
405 // If prevFocus is on screen at all, we just use it.
406 if (prevFocus.hasSelection()) {
407 QRectF prevRect = control->selectionRect(prevFocus);
408 if (viewRect.intersects(prevRect))
409 findOnScreen = false;
410 }
411
412 // Otherwise, we find a new anchor that's on screen.
413 // Basically, create a cursor with the last/first character
414 // on screen
415 if (findOnScreen) {
416 if (next)
417 prevFocus = control->cursorForPosition(QPointF(0, yOffset));
418 else
419 prevFocus = control->cursorForPosition(QPointF(control->size().width(), yOffset + height));
420 }
421 foundNextAnchor = control->findNextPrevAnchor(prevFocus, next, anchorToFocus);
422 } else if (prevFocus.hasSelection()) {
423 // Check the pathological case that the current anchor is higher
424 // than the screen, and just scroll through it in that case
425 QRectF prevRect = control->selectionRect(prevFocus);
426 if ((next && prevRect.bottom() > (yOffset + height)) ||
427 (!next && prevRect.top() < yOffset)) {
428 anchorToFocus = prevFocus;
429 focusedPos = scrollYOffset;
430 focusIt = true;
431 } else {
432 // This is the "normal" case - no scroll bar adjustments, no large anchors,
433 // and no wrapping.
434 foundNextAnchor = control->findNextPrevAnchor(prevFocus, next, anchorToFocus);
435 }
436 }
437
438 // If not found yet, see if we need to wrap
439 if (!focusIt && !foundNextAnchor) {
440 if (next) {
441 if (yOffset == vbar->maximum()) {
442 prevFocus.movePosition(QTextCursor::Start);
443 yOffset = scrollYOffset = 0;
444
445 // Refresh the rectangles
446 viewRect = QRectF(0, yOffset, control->size().width(), height);
447 newViewRect = QRectF(0, scrollYOffset, control->size().width(), height);
448 bothViewRects = viewRect.united(newViewRect);
449 }
450 } else {
451 if (yOffset == 0) {
452 prevFocus.movePosition(QTextCursor::End);
453 yOffset = scrollYOffset = vbar->maximum();
454
455 // Refresh the rectangles
456 viewRect = QRectF(0, yOffset, control->size().width(), height);
457 newViewRect = QRectF(0, scrollYOffset, control->size().width(), height);
458 bothViewRects = viewRect.united(newViewRect);
459 }
460 }
461
462 // Try looking now
463 foundNextAnchor = control->findNextPrevAnchor(prevFocus, next, anchorToFocus);
464 }
465
466 // If we did actually find an anchor to use...
467 if (foundNextAnchor) {
468 QRectF desiredRect = control->selectionRect(anchorToFocus);
469
470 // XXX This is an arbitrary heuristic
471 // Decide to focus an anchor if it will be at least be
472 // in the middle region of the screen after a scroll.
473 // This can result in partial anchors with focus, but
474 // insisting on links being completely visible before
475 // selecting them causes disparities between links that
476 // take up 90% of the screen height and those that take
477 // up e.g. 110%
478 // Obviously if a link is entirely visible, we still
479 // focus it.
480 if (bothViewRects.contains(desiredRect)
481 || bothViewRects.adjusted(0, visibleLinkAmount, 0, -visibleLinkAmount).intersects(desiredRect)) {
482 focusIt = true;
483
484 // We aim to put the new link in the middle of the screen,
485 // unless the link is larger than the screen (we just move to
486 // display the first page of the link)
487 if (desiredRect.height() > height) {
488 if (next)
489 focusedPos = (int) desiredRect.top();
490 else
491 focusedPos = (int) desiredRect.bottom() - height;
492 } else
493 focusedPos = (int) ((desiredRect.top() + desiredRect.bottom()) / 2 - (height / 2));
494
495 // and clamp it to make sure we don't skip content.
496 if (next)
497 focusedPos = qBound(yOffset, focusedPos, scrollYOffset);
498 else
499 focusedPos = qBound(scrollYOffset, focusedPos, yOffset);
500 }
501 }
502
503 // If we didn't get a new anchor, check if the old one is still on screen when we scroll
504 // Note that big (larger than screen height) anchors also have some handling at the
505 // start of this function.
506 if (!focusIt && prevFocus.hasSelection()) {
507 QRectF desiredRect = control->selectionRect(prevFocus);
508 // XXX this may be better off also using the visibleLinkAmount value
509 if (newViewRect.intersects(desiredRect)) {
510 focusedPos = scrollYOffset;
511 focusIt = true;
512 anchorToFocus = prevFocus;
513 }
514 }
515
516 // setTextCursor ensures that the cursor is visible. save & restore
517 // the scroll bar values therefore
518 const int savedXOffset = hbar->value();
519
520 // Now actually process our decision
521 if (focusIt && control->setFocusToAnchor(anchorToFocus)) {
522 // Save the focus for next time
523 prevFocus = control->textCursor();
524
525 // Scroll
526 vbar->setValue(focusedPos);
527 lastKeypadScrollValue = focusedPos;
528 hbar->setValue(savedXOffset);
529
530 // Ensure that the new selection is highlighted.
531 const QString href = control->anchorAtCursor();
532 QUrl url = resolveUrl(href);
533 emitHighlighted(url);
534 } else {
535 // Scroll
536 vbar->setValue(scrollYOffset);
537 lastKeypadScrollValue = scrollYOffset;
538
539 // now make sure we don't have a focused anchor
540 QTextCursor cursor = control->textCursor();
541 cursor.clearSelection();
542
543 control->setTextCursor(cursor);
544
545 hbar->setValue(savedXOffset);
546 vbar->setValue(scrollYOffset);
547
548 emitHighlighted(QUrl());
549 }
550}
551#endif
552
553QTextBrowserPrivate::HistoryEntry QTextBrowserPrivate::createHistoryEntry() const
554{
555 HistoryEntry entry;
556 entry.url = q_func()->source();
557 entry.type = q_func()->sourceType();
558 entry.title = q_func()->documentTitle();
559 entry.hpos = hbar->value();
560 entry.vpos = vbar->value();
561
562 const QTextCursor cursor = control->textCursor();
563 if (control->cursorIsFocusIndicator()
564 && cursor.hasSelection()) {
565
566 entry.focusIndicatorPosition = cursor.position();
567 entry.focusIndicatorAnchor = cursor.anchor();
568 }
569 return entry;
570}
571
572void QTextBrowserPrivate::restoreHistoryEntry(const HistoryEntry &entry)
573{
574 setSource(url: entry.url, type: entry.type);
575 hbar->setValue(entry.hpos);
576 vbar->setValue(entry.vpos);
577 if (entry.focusIndicatorAnchor != -1 && entry.focusIndicatorPosition != -1) {
578 QTextCursor cursor(control->document());
579 cursor.setPosition(pos: entry.focusIndicatorAnchor);
580 cursor.setPosition(pos: entry.focusIndicatorPosition, mode: QTextCursor::KeepAnchor);
581 control->setTextCursor(cursor);
582 control->setCursorIsFocusIndicator(true);
583 }
584#ifdef QT_KEYPAD_NAVIGATION
585 lastKeypadScrollValue = vbar->value();
586 prevFocus = control->textCursor();
587
588 Q_Q(QTextBrowser);
589 const QString href = prevFocus.charFormat().anchorHref();
590 QUrl url = resolveUrl(href);
591 emitHighlighted(url);
592#endif
593}
594
595/*!
596 \class QTextBrowser
597 \brief The QTextBrowser class provides a rich text browser with hypertext navigation.
598
599 \ingroup richtext-processing
600 \inmodule QtWidgets
601
602 This class extends QTextEdit (in read-only mode), adding some navigation
603 functionality so that users can follow links in hypertext documents.
604
605 If you want to provide your users with an editable rich text editor,
606 use QTextEdit. If you want a text browser without hypertext navigation
607 use QTextEdit, and use QTextEdit::setReadOnly() to disable
608 editing. If you just need to display a small piece of rich text
609 use QLabel.
610
611 \section1 Document Source and Contents
612
613 The contents of QTextEdit are set with setHtml() or setPlainText(),
614 but QTextBrowser also implements the setSource() function, making it
615 possible to use a named document as the source text. The name is looked
616 up in a list of search paths and in the directory of the current document
617 factory.
618
619 If a document name ends with
620 an anchor (for example, "\c #anchor"), the text browser automatically
621 scrolls to that position (using scrollToAnchor()). When the user clicks
622 on a hyperlink, the browser will call setSource() itself with the link's
623 \c href value as argument. You can track the current source by connecting
624 to the sourceChanged() signal.
625
626 \section1 Navigation
627
628 QTextBrowser provides backward() and forward() slots which you can
629 use to implement Back and Forward buttons. The home() slot sets
630 the text to the very first document displayed. The anchorClicked()
631 signal is emitted when the user clicks an anchor. To override the
632 default navigation behavior of the browser, call the setSource()
633 function to supply new document text in a slot connected to this
634 signal.
635
636 If you want to load documents stored in the Qt resource system use
637 \c{qrc} as the scheme in the URL to load. For example, for the document
638 resource path \c{:/docs/index.html} use \c{qrc:/docs/index.html} as
639 the URL with setSource().
640
641 \sa QTextEdit, QTextDocument
642*/
643
644/*!
645 \property QTextBrowser::modified
646 \brief whether the contents of the text browser have been modified
647*/
648
649/*!
650 \property QTextBrowser::readOnly
651 \brief whether the text browser is read-only
652
653 By default, this property is \c true.
654*/
655
656/*!
657 \property QTextBrowser::undoRedoEnabled
658 \brief whether the text browser supports undo/redo operations
659
660 By default, this property is \c false.
661*/
662
663void QTextBrowserPrivate::init()
664{
665 Q_Q(QTextBrowser);
666 control->setTextInteractionFlags(Qt::TextBrowserInteraction);
667#ifndef QT_NO_CURSOR
668 viewport->setCursor(oldCursor);
669#endif
670 q->setAttribute(Qt::WA_InputMethodEnabled, on: shouldEnableInputMethod(texbrowser: q));
671 q->setUndoRedoEnabled(false);
672 viewport->setMouseTracking(true);
673 QObject::connect(sender: q->document(), SIGNAL(contentsChanged()), receiver: q, SLOT(_q_documentModified()));
674 QObject::connect(sender: control, SIGNAL(linkActivated(QString)),
675 receiver: q, SLOT(_q_activateAnchor(QString)));
676 QObject::connect(sender: control, SIGNAL(linkHovered(QString)),
677 receiver: q, SLOT(_q_highlightLink(QString)));
678}
679
680/*!
681 Constructs an empty QTextBrowser with parent \a parent.
682*/
683QTextBrowser::QTextBrowser(QWidget *parent)
684 : QTextEdit(*new QTextBrowserPrivate, parent)
685{
686 Q_D(QTextBrowser);
687 d->init();
688}
689
690
691/*!
692 \internal
693*/
694QTextBrowser::~QTextBrowser()
695{
696}
697
698/*!
699 \property QTextBrowser::source
700 \brief the name of the displayed document.
701
702 This is a an invalid url if no document is displayed or if the
703 source is unknown.
704
705 When setting this property QTextBrowser tries to find a document
706 with the specified name in the paths of the searchPaths property
707 and directory of the current source, unless the value is an absolute
708 file path. It also checks for optional anchors and scrolls the document
709 accordingly
710
711 If the first tag in the document is \c{<qt type=detail>}, the
712 document is displayed as a popup rather than as new document in
713 the browser window itself. Otherwise, the document is displayed
714 normally in the text browser with the text set to the contents of
715 the named document with \l QTextDocument::setHtml() or
716 \l QTextDocument::setMarkdown(), depending on whether the filename ends
717 with any of the known Markdown file extensions.
718
719 If you would like to avoid automatic type detection
720 and specify the type explicitly, call setSource() rather than
721 setting this property.
722
723 By default, this property contains an empty URL.
724*/
725QUrl QTextBrowser::source() const
726{
727 Q_D(const QTextBrowser);
728 if (d->stack.isEmpty())
729 return QUrl();
730 else
731 return d->stack.top().url;
732}
733
734/*!
735 \property QTextBrowser::sourceType
736 \brief the type of the displayed document
737
738 This is QTextDocument::UnknownResource if no document is displayed or if
739 the type of the source is unknown. Otherwise it holds the type that was
740 detected, or the type that was specified when setSource() was called.
741*/
742QTextDocument::ResourceType QTextBrowser::sourceType() const
743{
744 Q_D(const QTextBrowser);
745 if (d->stack.isEmpty())
746 return QTextDocument::UnknownResource;
747 else
748 return d->stack.top().type;
749}
750
751/*!
752 \property QTextBrowser::searchPaths
753 \brief the search paths used by the text browser to find supporting
754 content
755
756 QTextBrowser uses this list to locate images and documents.
757
758 By default, this property contains an empty string list.
759*/
760
761QStringList QTextBrowser::searchPaths() const
762{
763 Q_D(const QTextBrowser);
764 return d->searchPaths;
765}
766
767void QTextBrowser::setSearchPaths(const QStringList &paths)
768{
769 Q_D(QTextBrowser);
770 d->searchPaths = paths;
771}
772
773/*!
774 Reloads the current set source.
775*/
776void QTextBrowser::reload()
777{
778 Q_D(QTextBrowser);
779 QUrl s = d->currentURL;
780 d->currentURL = QUrl();
781 setSource(name: s, type: d->currentType);
782}
783
784/*!
785 Attempts to load the document at the given \a url with the specified \a type.
786
787 If \a type is \l {QTextDocument::UnknownResource}{UnknownResource}
788 (the default), the document type will be detected: that is, if the url ends
789 with an extension of \c{.md}, \c{.mkd} or \c{.markdown}, the document will be
790 loaded via \l QTextDocument::setMarkdown(); otherwise it will be loaded via
791 \l QTextDocument::setHtml(). This detection can be bypassed by specifying
792 the \a type explicitly.
793*/
794void QTextBrowser::setSource(const QUrl &url, QTextDocument::ResourceType type)
795{
796 doSetSource(name: url, type);
797}
798
799/*!
800 Attempts to load the document at the given \a url with the specified \a type.
801
802 setSource() calls doSetSource. In Qt 5, setSource(const QUrl &url) was virtual.
803 In Qt 6, doSetSource() is virtual instead, so that it can be overridden in subclasses.
804*/
805void QTextBrowser::doSetSource(const QUrl &url, QTextDocument::ResourceType type)
806{
807 Q_D(QTextBrowser);
808
809 const QTextBrowserPrivate::HistoryEntry historyEntry = d->createHistoryEntry();
810
811 d->setSource(url, type);
812
813 if (!url.isValid())
814 return;
815
816 // the same url you are already watching?
817 if (!d->stack.isEmpty() && d->stack.top().url == url)
818 return;
819
820 if (!d->stack.isEmpty())
821 d->stack.top() = historyEntry;
822
823 QTextBrowserPrivate::HistoryEntry entry;
824 entry.url = url;
825 entry.type = d->currentType;
826 entry.title = documentTitle();
827 entry.hpos = 0;
828 entry.vpos = 0;
829 d->stack.push(t: entry);
830
831 emit backwardAvailable(d->stack.size() > 1);
832
833 if (!d->forwardStack.isEmpty() && d->forwardStack.top().url == url) {
834 d->forwardStack.pop();
835 emit forwardAvailable(d->forwardStack.size() > 0);
836 } else {
837 d->forwardStack.clear();
838 emit forwardAvailable(false);
839 }
840
841 emit historyChanged();
842}
843
844/*!
845 \fn void QTextBrowser::backwardAvailable(bool available)
846
847 This signal is emitted when the availability of backward()
848 changes. \a available is false when the user is at home();
849 otherwise it is true.
850*/
851
852/*!
853 \fn void QTextBrowser::forwardAvailable(bool available)
854
855 This signal is emitted when the availability of forward() changes.
856 \a available is true after the user navigates backward() and false
857 when the user navigates or goes forward().
858*/
859
860/*!
861 \fn void QTextBrowser::historyChanged()
862 \since 4.4
863
864 This signal is emitted when the history changes.
865
866 \sa historyTitle(), historyUrl()
867*/
868
869/*!
870 \fn void QTextBrowser::sourceChanged(const QUrl &src)
871
872 This signal is emitted when the source has changed, \a src
873 being the new source.
874
875 Source changes happen both programmatically when calling
876 setSource(), forward(), backward() or home() or when the user
877 clicks on links or presses the equivalent key sequences.
878*/
879
880/*! \fn void QTextBrowser::highlighted(const QUrl &link)
881
882 This signal is emitted when the user has selected but not
883 activated an anchor in the document. The URL referred to by the
884 anchor is passed in \a link.
885*/
886
887/*!
888 \fn void QTextBrowser::anchorClicked(const QUrl &link)
889
890 This signal is emitted when the user clicks an anchor. The
891 URL referred to by the anchor is passed in \a link.
892
893 Note that the browser will automatically handle navigation to the
894 location specified by \a link unless the openLinks property
895 is set to false or you call setSource() in a slot connected.
896 This mechanism is used to override the default navigation features of the browser.
897*/
898
899/*!
900 Changes the document displayed to the previous document in the
901 list of documents built by navigating links. Does nothing if there
902 is no previous document.
903
904 \sa forward(), backwardAvailable()
905*/
906void QTextBrowser::backward()
907{
908 Q_D(QTextBrowser);
909 if (d->stack.size() <= 1)
910 return;
911
912 // Update the history entry
913 d->forwardStack.push(t: d->createHistoryEntry());
914 d->stack.pop(); // throw away the old version of the current entry
915 d->restoreHistoryEntry(entry: d->stack.top()); // previous entry
916 emit backwardAvailable(d->stack.size() > 1);
917 emit forwardAvailable(true);
918 emit historyChanged();
919}
920
921/*!
922 Changes the document displayed to the next document in the list of
923 documents built by navigating links. Does nothing if there is no
924 next document.
925
926 \sa backward(), forwardAvailable()
927*/
928void QTextBrowser::forward()
929{
930 Q_D(QTextBrowser);
931 if (d->forwardStack.isEmpty())
932 return;
933 if (!d->stack.isEmpty()) {
934 // Update the history entry
935 d->stack.top() = d->createHistoryEntry();
936 }
937 d->stack.push(t: d->forwardStack.pop());
938 d->restoreHistoryEntry(entry: d->stack.top());
939 emit backwardAvailable(true);
940 emit forwardAvailable(!d->forwardStack.isEmpty());
941 emit historyChanged();
942}
943
944/*!
945 Changes the document displayed to be the first document from
946 the history.
947*/
948void QTextBrowser::home()
949{
950 Q_D(QTextBrowser);
951 if (d->home.isValid())
952 setSource(url: d->home);
953}
954
955/*!
956 The event \a ev is used to provide the following keyboard shortcuts:
957 \table
958 \header \li Keypress \li Action
959 \row \li Alt+Left Arrow \li \l backward()
960 \row \li Alt+Right Arrow \li \l forward()
961 \row \li Alt+Up Arrow \li \l home()
962 \endtable
963*/
964void QTextBrowser::keyPressEvent(QKeyEvent *ev)
965{
966#ifdef QT_KEYPAD_NAVIGATION
967 Q_D(QTextBrowser);
968 switch (ev->key()) {
969 case Qt::Key_Select:
970 if (QApplicationPrivate::keypadNavigationEnabled()) {
971 if (!hasEditFocus()) {
972 setEditFocus(true);
973 return;
974 } else {
975 QTextCursor cursor = d->control->textCursor();
976 QTextCharFormat charFmt = cursor.charFormat();
977 if (!cursor.hasSelection() || charFmt.anchorHref().isEmpty()) {
978 ev->accept();
979 return;
980 }
981 }
982 }
983 break;
984 case Qt::Key_Back:
985 if (QApplicationPrivate::keypadNavigationEnabled()) {
986 if (hasEditFocus()) {
987 setEditFocus(false);
988 ev->accept();
989 return;
990 }
991 }
992 QTextEdit::keyPressEvent(ev);
993 return;
994 default:
995 if (QApplicationPrivate::keypadNavigationEnabled() && !hasEditFocus()) {
996 ev->ignore();
997 return;
998 }
999 }
1000#endif
1001
1002 if (ev->modifiers() & Qt::AltModifier) {
1003 switch (ev->key()) {
1004 case Qt::Key_Right:
1005 forward();
1006 ev->accept();
1007 return;
1008 case Qt::Key_Left:
1009 backward();
1010 ev->accept();
1011 return;
1012 case Qt::Key_Up:
1013 home();
1014 ev->accept();
1015 return;
1016 }
1017 }
1018#ifdef QT_KEYPAD_NAVIGATION
1019 else {
1020 if (ev->key() == Qt::Key_Up) {
1021 d->keypadMove(false);
1022 return;
1023 } else if (ev->key() == Qt::Key_Down) {
1024 d->keypadMove(true);
1025 return;
1026 }
1027 }
1028#endif
1029 QTextEdit::keyPressEvent(e: ev);
1030}
1031
1032/*!
1033 \reimp
1034*/
1035void QTextBrowser::mouseMoveEvent(QMouseEvent *e)
1036{
1037 QTextEdit::mouseMoveEvent(e);
1038}
1039
1040/*!
1041 \reimp
1042*/
1043void QTextBrowser::mousePressEvent(QMouseEvent *e)
1044{
1045 QTextEdit::mousePressEvent(e);
1046}
1047
1048/*!
1049 \reimp
1050*/
1051void QTextBrowser::mouseReleaseEvent(QMouseEvent *e)
1052{
1053 QTextEdit::mouseReleaseEvent(e);
1054}
1055
1056/*!
1057 \reimp
1058*/
1059void QTextBrowser::focusOutEvent(QFocusEvent *ev)
1060{
1061#ifndef QT_NO_CURSOR
1062 Q_D(QTextBrowser);
1063 d->viewport->setCursor((!(d->control->textInteractionFlags() & Qt::TextEditable)) ? d->oldCursor : Qt::IBeamCursor);
1064#endif
1065 QTextEdit::focusOutEvent(e: ev);
1066}
1067
1068/*!
1069 \reimp
1070*/
1071bool QTextBrowser::focusNextPrevChild(bool next)
1072{
1073 Q_D(QTextBrowser);
1074 if (d->control->setFocusToNextOrPreviousAnchor(next)) {
1075#ifdef QT_KEYPAD_NAVIGATION
1076 // Might need to synthesize a highlight event.
1077 if (d->prevFocus != d->control->textCursor() && d->control->textCursor().hasSelection()) {
1078 const QString href = d->control->anchorAtCursor();
1079 QUrl url = d->resolveUrl(href);
1080 emitHighlighted(url);
1081 }
1082 d->prevFocus = d->control->textCursor();
1083#endif
1084 return true;
1085 } else {
1086#ifdef QT_KEYPAD_NAVIGATION
1087 // We assume we have no highlight now.
1088 emitHighlighted(QUrl());
1089#endif
1090 }
1091 return QTextEdit::focusNextPrevChild(next);
1092}
1093
1094/*!
1095 \reimp
1096*/
1097void QTextBrowser::paintEvent(QPaintEvent *e)
1098{
1099 Q_D(QTextBrowser);
1100 QPainter p(d->viewport);
1101 d->paint(p: &p, e);
1102}
1103
1104/*!
1105 This function is called when the document is loaded and for
1106 each image in the document. The \a type indicates the type of resource
1107 to be loaded. An invalid QVariant is returned if the resource cannot be
1108 loaded.
1109
1110 The default implementation ignores \a type and tries to locate
1111 the resources by interpreting \a name as a file name. If it is
1112 not an absolute path it tries to find the file in the paths of
1113 the \l searchPaths property and in the same directory as the
1114 current source. On success, the result is a QVariant that stores
1115 a QByteArray with the contents of the file.
1116
1117 If you reimplement this function, you can return other QVariant
1118 types. The table below shows which variant types are supported
1119 depending on the resource type:
1120
1121 \table
1122 \header \li ResourceType \li QMetaType::Type
1123 \row \li QTextDocument::HtmlResource \li QString or QByteArray
1124 \row \li QTextDocument::ImageResource \li QImage, QPixmap or QByteArray
1125 \row \li QTextDocument::StyleSheetResource \li QString or QByteArray
1126 \row \li QTextDocument::MarkdownResource \li QString or QByteArray
1127 \endtable
1128*/
1129QVariant QTextBrowser::loadResource(int /*type*/, const QUrl &name)
1130{
1131 Q_D(QTextBrowser);
1132
1133 QByteArray data;
1134 QString fileName = d->findFile(name: d->resolveUrl(url: name));
1135 if (fileName.isEmpty())
1136 return QVariant();
1137 QFile f(fileName);
1138 if (f.open(flags: QFile::ReadOnly)) {
1139 data = f.readAll();
1140 f.close();
1141 } else {
1142 return QVariant();
1143 }
1144
1145 return data;
1146}
1147
1148/*!
1149 \since 4.2
1150
1151 Returns \c true if the text browser can go backward in the document history
1152 using backward().
1153
1154 \sa backwardAvailable(), backward()
1155*/
1156bool QTextBrowser::isBackwardAvailable() const
1157{
1158 Q_D(const QTextBrowser);
1159 return d->stack.size() > 1;
1160}
1161
1162/*!
1163 \since 4.2
1164
1165 Returns \c true if the text browser can go forward in the document history
1166 using forward().
1167
1168 \sa forwardAvailable(), forward()
1169*/
1170bool QTextBrowser::isForwardAvailable() const
1171{
1172 Q_D(const QTextBrowser);
1173 return !d->forwardStack.isEmpty();
1174}
1175
1176/*!
1177 \since 4.2
1178
1179 Clears the history of visited documents and disables the forward and
1180 backward navigation.
1181
1182 \sa backward(), forward()
1183*/
1184void QTextBrowser::clearHistory()
1185{
1186 Q_D(QTextBrowser);
1187 d->forwardStack.clear();
1188 if (!d->stack.isEmpty()) {
1189 QTextBrowserPrivate::HistoryEntry historyEntry = d->stack.top();
1190 d->stack.clear();
1191 d->stack.push(t: historyEntry);
1192 d->home = historyEntry.url;
1193 }
1194 emit forwardAvailable(false);
1195 emit backwardAvailable(false);
1196 emit historyChanged();
1197}
1198
1199/*!
1200 Returns the url of the HistoryItem.
1201
1202 \table
1203 \header \li Input \li Return
1204 \row \li \a{i} < 0 \li \l backward() history
1205 \row \li\a{i} == 0 \li current, see QTextBrowser::source()
1206 \row \li \a{i} > 0 \li \l forward() history
1207 \endtable
1208
1209 \since 4.4
1210*/
1211QUrl QTextBrowser::historyUrl(int i) const
1212{
1213 Q_D(const QTextBrowser);
1214 return d->history(i).url;
1215}
1216
1217/*!
1218 Returns the documentTitle() of the HistoryItem.
1219
1220 \table
1221 \header \li Input \li Return
1222 \row \li \a{i} < 0 \li \l backward() history
1223 \row \li \a{i} == 0 \li current, see QTextBrowser::source()
1224 \row \li \a{i} > 0 \li \l forward() history
1225 \endtable
1226
1227 \snippet code/src_gui_widgets_qtextbrowser.cpp 0
1228
1229 \since 4.4
1230*/
1231QString QTextBrowser::historyTitle(int i) const
1232{
1233 Q_D(const QTextBrowser);
1234 return d->history(i).title;
1235}
1236
1237
1238/*!
1239 Returns the number of locations forward in the history.
1240
1241 \since 4.4
1242*/
1243int QTextBrowser::forwardHistoryCount() const
1244{
1245 Q_D(const QTextBrowser);
1246 return d->forwardStack.size();
1247}
1248
1249/*!
1250 Returns the number of locations backward in the history.
1251
1252 \since 4.4
1253*/
1254int QTextBrowser::backwardHistoryCount() const
1255{
1256 Q_D(const QTextBrowser);
1257 return d->stack.size()-1;
1258}
1259
1260/*!
1261 \property QTextBrowser::openExternalLinks
1262 \since 4.2
1263
1264 Specifies whether QTextBrowser should automatically open links to external
1265 sources using QDesktopServices::openUrl() instead of emitting the
1266 anchorClicked signal. Links are considered external if their scheme is
1267 neither file or qrc.
1268
1269 The default value is false.
1270*/
1271bool QTextBrowser::openExternalLinks() const
1272{
1273 Q_D(const QTextBrowser);
1274 return d->openExternalLinks;
1275}
1276
1277void QTextBrowser::setOpenExternalLinks(bool open)
1278{
1279 Q_D(QTextBrowser);
1280 d->openExternalLinks = open;
1281}
1282
1283/*!
1284 \property QTextBrowser::openLinks
1285 \since 4.3
1286
1287 This property specifies whether QTextBrowser should automatically open links the user tries to
1288 activate by mouse or keyboard.
1289
1290 Regardless of the value of this property the anchorClicked signal is always emitted.
1291
1292 The default value is true.
1293*/
1294
1295bool QTextBrowser::openLinks() const
1296{
1297 Q_D(const QTextBrowser);
1298 return d->openLinks;
1299}
1300
1301void QTextBrowser::setOpenLinks(bool open)
1302{
1303 Q_D(QTextBrowser);
1304 d->openLinks = open;
1305}
1306
1307/*! \reimp */
1308bool QTextBrowser::event(QEvent *e)
1309{
1310 return QTextEdit::event(e);
1311}
1312
1313QT_END_NAMESPACE
1314
1315#include "moc_qtextbrowser.cpp"
1316

source code of qtbase/src/widgets/widgets/qtextbrowser.cpp