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: |
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) |
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 | |
168 | 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(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 | |
201 | 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(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 | |
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(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 | |
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(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 | |
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(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 |
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 | |
587 | 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 | |
606 | void 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 | |
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, 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 | */ |
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 | */ |
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 | */ |
759 | 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 | */ |
776 | 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 | |
795 | QStringList QTextBrowser::searchPaths() const |
796 | { |
797 | Q_D(const QTextBrowser); |
798 | return d->searchPaths; |
799 | } |
800 | |
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 | */ |
810 | void 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) |
819 | void 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 | */ |
835 | void 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 |
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(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 | */ |
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(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 | */ |
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(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 | */ |
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 | */ |
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(e: ev); |
1083 | } |
1084 | |
1085 | /*! |
1086 | \reimp |
1087 | */ |
1088 | void QTextBrowser::mouseMoveEvent(QMouseEvent *e) |
1089 | { |
1090 | QTextEdit::mouseMoveEvent(e); |
1091 | } |
1092 | |
1093 | /*! |
1094 | \reimp |
1095 | */ |
1096 | void QTextBrowser::mousePressEvent(QMouseEvent *e) |
1097 | { |
1098 | QTextEdit::mousePressEvent(e); |
1099 | } |
1100 | |
1101 | /*! |
1102 | \reimp |
1103 | */ |
1104 | void QTextBrowser::mouseReleaseEvent(QMouseEvent *e) |
1105 | { |
1106 | QTextEdit::mouseReleaseEvent(e); |
1107 | } |
1108 | |
1109 | /*! |
1110 | \reimp |
1111 | */ |
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(e: ev); |
1119 | } |
1120 | |
1121 | /*! |
1122 | \reimp |
1123 | */ |
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 | */ |
1150 | void 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 | */ |
1182 | QVariant 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 | */ |
1209 | 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 | */ |
1223 | 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 | */ |
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(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 | */ |
1264 | 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 | */ |
1284 | 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 | */ |
1296 | 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 | */ |
1307 | 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 | */ |
1324 | bool QTextBrowser::openExternalLinks() const |
1325 | { |
1326 | Q_D(const QTextBrowser); |
1327 | return d->openExternalLinks; |
1328 | } |
1329 | |
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 | |
1348 | bool QTextBrowser::openLinks() const |
1349 | { |
1350 | Q_D(const QTextBrowser); |
1351 | return d->openLinks; |
1352 | } |
1353 | |
1354 | void QTextBrowser::setOpenLinks(bool open) |
1355 | { |
1356 | Q_D(QTextBrowser); |
1357 | d->openLinks = open; |
1358 | } |
1359 | |
1360 | /*! \reimp */ |
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 |
Definitions
- lcBrowser
- QTextBrowserPrivate
- QTextBrowserPrivate
- HistoryEntry
- HistoryEntry
- history
- _q_documentModified
- resolveUrl
- emitHighlighted
- findFile
- resolveUrl
- _q_activateAnchor
- _q_highlightLink
- setSource
- createHistoryEntry
- restoreHistoryEntry
- init
- QTextBrowser
- ~QTextBrowser
- source
- sourceType
- searchPaths
- setSearchPaths
- reload
- setSource
- setSource
- doSetSource
- backward
- forward
- home
- keyPressEvent
- mouseMoveEvent
- mousePressEvent
- mouseReleaseEvent
- focusOutEvent
- focusNextPrevChild
- paintEvent
- loadResource
- isBackwardAvailable
- isForwardAvailable
- clearHistory
- historyUrl
- historyTitle
- forwardHistoryCount
- backwardHistoryCount
- openExternalLinks
- setOpenExternalLinks
- openLinks
- setOpenLinks
Learn Advanced QML with KDAB
Find out more