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