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