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