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