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(QUrl(url)); } |
149 | |
150 | #ifdef QT_KEYPAD_NAVIGATION |
151 | void keypadMove(bool next); |
152 | QTextCursor prevFocus; |
153 | int lastKeypadScrollValue; |
154 | #endif |
155 | }; |
156 | Q_DECLARE_TYPEINFO(QTextBrowserPrivate::HistoryEntry, Q_MOVABLE_TYPE); |
157 | |
158 | QString 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 | |
191 | QUrl 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 | |
216 | void 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 | |
257 | void 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 | |
280 | void 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 |
398 | void 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 | |
584 | QTextBrowserPrivate::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 | |
603 | void 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 | |
695 | void 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 | */ |
715 | QTextBrowser::QTextBrowser(QWidget *parent) |
716 | : QTextEdit(*new QTextBrowserPrivate, parent) |
717 | { |
718 | Q_D(QTextBrowser); |
719 | d->init(); |
720 | } |
721 | |
722 | |
723 | /*! |
724 | \internal |
725 | */ |
726 | QTextBrowser::~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 | */ |
757 | QUrl 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 | */ |
774 | QTextDocument::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 | |
793 | QStringList QTextBrowser::searchPaths() const |
794 | { |
795 | Q_D(const QTextBrowser); |
796 | return d->searchPaths; |
797 | } |
798 | |
799 | void QTextBrowser::setSearchPaths(const QStringList &paths) |
800 | { |
801 | Q_D(QTextBrowser); |
802 | d->searchPaths = paths; |
803 | } |
804 | |
805 | /*! |
806 | Reloads the current set source. |
807 | */ |
808 | void 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) |
817 | void 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 | */ |
833 | void 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 |
846 | void 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 | */ |
956 | void 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 | */ |
978 | void 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 | */ |
998 | void 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 | */ |
1014 | void 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 | */ |
1085 | void QTextBrowser::mouseMoveEvent(QMouseEvent *e) |
1086 | { |
1087 | QTextEdit::mouseMoveEvent(e); |
1088 | } |
1089 | |
1090 | /*! |
1091 | \reimp |
1092 | */ |
1093 | void QTextBrowser::mousePressEvent(QMouseEvent *e) |
1094 | { |
1095 | QTextEdit::mousePressEvent(e); |
1096 | } |
1097 | |
1098 | /*! |
1099 | \reimp |
1100 | */ |
1101 | void QTextBrowser::mouseReleaseEvent(QMouseEvent *e) |
1102 | { |
1103 | QTextEdit::mouseReleaseEvent(e); |
1104 | } |
1105 | |
1106 | /*! |
1107 | \reimp |
1108 | */ |
1109 | void 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 | */ |
1121 | bool 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 | */ |
1149 | void 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 | */ |
1181 | QVariant 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 | */ |
1208 | bool 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 | */ |
1222 | bool 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 | */ |
1236 | void 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 | */ |
1263 | QUrl 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 | */ |
1283 | QString 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 | */ |
1295 | int 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 | */ |
1306 | int 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 | */ |
1323 | bool QTextBrowser::openExternalLinks() const |
1324 | { |
1325 | Q_D(const QTextBrowser); |
1326 | return d->openExternalLinks; |
1327 | } |
1328 | |
1329 | void 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 | |
1347 | bool QTextBrowser::openLinks() const |
1348 | { |
1349 | Q_D(const QTextBrowser); |
1350 | return d->openLinks; |
1351 | } |
1352 | |
1353 | void QTextBrowser::setOpenLinks(bool open) |
1354 | { |
1355 | Q_D(QTextBrowser); |
1356 | d->openLinks = open; |
1357 | } |
1358 | |
1359 | /*! \reimp */ |
1360 | bool QTextBrowser::event(QEvent *e) |
1361 | { |
1362 | return QTextEdit::event(e); |
1363 | } |
1364 | |
1365 | QT_END_NAMESPACE |
1366 | |
1367 | #include "moc_qtextbrowser.cpp" |
1368 | |