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