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(url: QUrl(url)); }
149
150#ifdef QT_KEYPAD_NAVIGATION
151 void keypadMove(bool next);
152 QTextCursor prevFocus;
153 int lastKeypadScrollValue;
154#endif
155 void emitHighlighted(const QUrl &url)
156 {
157 Q_Q(QTextBrowser);
158 emit q->highlighted(url);
159#if QT_DEPRECATED_SINCE(5, 15)
160QT_WARNING_PUSH
161QT_WARNING_DISABLE_DEPRECATED
162 emit q->highlighted(url.toString());
163#endif
164 }
165};
166Q_DECLARE_TYPEINFO(QTextBrowserPrivate::HistoryEntry, Q_MOVABLE_TYPE);
167
168QString 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(t: searchPaths)) {
191 if (!path.endsWith(c: QLatin1Char('/')))
192 path.append(c: QLatin1Char('/'));
193 path.append(s: fileName);
194 if (QFileInfo(path).isReadable())
195 return path;
196 }
197
198 return fileName;
199}
200
201QUrl 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(relative: 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(localfile: fi.absolutePath() + QDir::separator()).resolved(relative: url);
221 }
222
223 return url;
224}
225
226void 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(url: 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
267void 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(url: QUrl());
276 } else {
277#ifndef QT_NO_CURSOR
278 viewport->setCursor(Qt::PointingHandCursor);
279#endif
280
281 const QUrl url = resolveUrl(url: anchor);
282 emitHighlighted(url);
283 }
284}
285
286void 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(fragment: QString());
301 QUrl newUrlWithoutFragment = currentURL.resolved(relative: url);
302 newUrlWithoutFragment.setFragment(fragment: QString());
303 QString fileName = url.fileName();
304 if (type == QTextDocument::UnknownResource) {
305#if QT_CONFIG(textmarkdownreader)
306 if (fileName.endsWith(s: QLatin1String(".md")) ||
307 fileName.endsWith(s: QLatin1String(".mkd")) ||
308 fileName.endsWith(s: 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, name: 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(str: data.toByteArray());
332 }
333 }
334 if (Q_UNLIKELY(txt.isEmpty()))
335 qWarning(msg: "QTextBrowser: No document for %s", url.toString().toLatin1().constData());
336
337 if (q->isVisible()) {
338 const QStringRef firstTag = txt.leftRef(n: txt.indexOf(c: QLatin1Char('>')) + 1);
339 if (firstTag.startsWith(s: QLatin1String("<qt")) && firstTag.contains(s: QLatin1String("type")) && firstTag.contains(s: QLatin1String("detail"))) {
340#ifndef QT_NO_CURSOR
341 QGuiApplication::restoreOverrideCursor();
342#endif
343#if QT_CONFIG(whatsthis)
344 QWhatsThis::showText(pos: QCursor::pos(), text: txt, w: 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(options: QUrl::RemoveFilename);
362 if (!baseUrl.path().isEmpty())
363 q->document()->setBaseUrl(baseUrl);
364 q->document()->setMetaInformation(info: 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(name: 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
403void 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
587QTextBrowserPrivate::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
606void QTextBrowserPrivate::restoreHistoryEntry(const HistoryEntry &entry)
607{
608 setSource(url: entry.url, type: 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(pos: entry.focusIndicatorAnchor);
614 cursor.setPosition(pos: entry.focusIndicatorPosition, mode: 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
697void 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, on: !q->isReadOnly());
705 q->setUndoRedoEnabled(false);
706 viewport->setMouseTracking(true);
707 QObject::connect(sender: q->document(), SIGNAL(contentsChanged()), receiver: q, SLOT(_q_documentModified()));
708 QObject::connect(sender: control, SIGNAL(linkActivated(QString)),
709 receiver: q, SLOT(_q_activateAnchor(QString)));
710 QObject::connect(sender: control, SIGNAL(linkHovered(QString)),
711 receiver: q, SLOT(_q_highlightLink(QString)));
712}
713
714/*!
715 Constructs an empty QTextBrowser with parent \a parent.
716*/
717QTextBrowser::QTextBrowser(QWidget *parent)
718 : QTextEdit(*new QTextBrowserPrivate, parent)
719{
720 Q_D(QTextBrowser);
721 d->init();
722}
723
724
725/*!
726 \internal
727*/
728QTextBrowser::~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*/
759QUrl 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*/
776QTextDocument::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
795QStringList QTextBrowser::searchPaths() const
796{
797 Q_D(const QTextBrowser);
798 return d->searchPaths;
799}
800
801void QTextBrowser::setSearchPaths(const QStringList &paths)
802{
803 Q_D(QTextBrowser);
804 d->searchPaths = paths;
805}
806
807/*!
808 Reloads the current set source.
809*/
810void QTextBrowser::reload()
811{
812 Q_D(QTextBrowser);
813 QUrl s = d->currentURL;
814 d->currentURL = QUrl();
815 setSource(name: s, type: d->currentType);
816}
817
818#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
819void QTextBrowser::setSource(const QUrl &url)
820{
821 setSource(name: url, type: 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*/
835void QTextBrowser::setSource(const QUrl &url, QTextDocument::ResourceType type)
836{
837 doSetSource(name: 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
848void 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(t: 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*/
959void 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(t: d->createHistoryEntry());
967 d->stack.pop(); // throw away the old version of the current entry
968 d->restoreHistoryEntry(entry: 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*/
981void 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(t: d->forwardStack.pop());
991 d->restoreHistoryEntry(entry: 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*/
1001void 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*/
1017void 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(e: ev);
1083}
1084
1085/*!
1086 \reimp
1087*/
1088void QTextBrowser::mouseMoveEvent(QMouseEvent *e)
1089{
1090 QTextEdit::mouseMoveEvent(e);
1091}
1092
1093/*!
1094 \reimp
1095*/
1096void QTextBrowser::mousePressEvent(QMouseEvent *e)
1097{
1098 QTextEdit::mousePressEvent(e);
1099}
1100
1101/*!
1102 \reimp
1103*/
1104void QTextBrowser::mouseReleaseEvent(QMouseEvent *e)
1105{
1106 QTextEdit::mouseReleaseEvent(e);
1107}
1108
1109/*!
1110 \reimp
1111*/
1112void 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(e: ev);
1119}
1120
1121/*!
1122 \reimp
1123*/
1124bool 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*/
1150void QTextBrowser::paintEvent(QPaintEvent *e)
1151{
1152 Q_D(QTextBrowser);
1153 QPainter p(d->viewport);
1154 d->paint(p: &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*/
1182QVariant QTextBrowser::loadResource(int /*type*/, const QUrl &name)
1183{
1184 Q_D(QTextBrowser);
1185
1186 QByteArray data;
1187 QString fileName = d->findFile(name: d->resolveUrl(url: name));
1188 if (fileName.isEmpty())
1189 return QVariant();
1190 QFile f(fileName);
1191 if (f.open(flags: 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*/
1209bool 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*/
1223bool 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*/
1237void 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(t: 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*/
1264QUrl 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*/
1284QString 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*/
1296int 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*/
1307int 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*/
1324bool QTextBrowser::openExternalLinks() const
1325{
1326 Q_D(const QTextBrowser);
1327 return d->openExternalLinks;
1328}
1329
1330void 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
1348bool QTextBrowser::openLinks() const
1349{
1350 Q_D(const QTextBrowser);
1351 return d->openLinks;
1352}
1353
1354void QTextBrowser::setOpenLinks(bool open)
1355{
1356 Q_D(QTextBrowser);
1357 d->openLinks = open;
1358}
1359
1360/*! \reimp */
1361bool QTextBrowser::event(QEvent *e)
1362{
1363 return QTextEdit::event(e);
1364}
1365
1366QT_END_NAMESPACE
1367
1368#include "moc_qtextbrowser.cpp"
1369

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