Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
helpviewer.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "helpviewer.h"
6
9#include "tracer.h"
10
11#include <QtCore/QFileInfo>
12#include <QtCore/QMimeDatabase>
13#include <QtCore/QStringBuilder>
14#include <QtCore/QTemporaryFile>
15#include <QtCore/QXmlStreamReader>
16#include <QtCore/QXmlStreamWriter>
17
18#include <QtGui/QDesktopServices>
19#if QT_CONFIG(clipboard)
20#include <QtGui/QClipboard>
21#endif
22#include <QtGui/QGuiApplication>
23#include <QtGui/QImageReader>
24#include <QtGui/QStyleHints>
25#include <QtGui/QWheelEvent>
26
27#include <QtWidgets/QMenu>
28#include <QtWidgets/QScrollBar>
29#include <QtWidgets/QVBoxLayout>
30
31#include <QtHelp/QHelpEngineCore>
32
33#include <qlitehtmlwidget.h>
34
35QT_BEGIN_NAMESPACE
36
37using namespace Qt::StringLiterals;
38
39const int kMaxHistoryItems = 20;
40constexpr bool kHasClipboard = QT_CONFIG(clipboard);
41
42static void setClipboardText(const QString &text)
43{
44#if QT_CONFIG(clipboard)
45 QGuiApplication::clipboard()->setText(text);
46#else
47 Q_UNUSED(text)
48#endif
49}
50
51const struct ExtensionMap {
52 const char *extension;
53 const char *mimeType;
54} extensionMap[] = {
55 { ".bmp", "image/bmp" },
56 { ".css", "text/css" },
57 { ".gif", "image/gif" },
58 { ".html", "text/html" },
59 { ".htm", "text/html" },
60 { ".ico", "image/x-icon" },
61 { ".jpeg", "image/jpeg" },
62 { ".jpg", "image/jpeg" },
63 { ".js", "application/x-javascript" },
64 { ".mng", "video/x-mng" },
65 { ".pbm", "image/x-portable-bitmap" },
66 { ".pgm", "image/x-portable-graymap" },
67 { ".pdf", nullptr },
68 { ".png", "image/png" },
69 { ".ppm", "image/x-portable-pixmap" },
70 { ".rss", "application/rss+xml" },
71 { ".svg", "image/svg+xml" },
72 { ".svgz", "image/svg+xml" },
73 { ".text", "text/plain" },
74 { ".tif", "image/tiff" },
75 { ".tiff", "image/tiff" },
76 { ".txt", "text/plain" },
77 { ".xbm", "image/x-xbitmap" },
78 { ".xml", "text/xml" },
79 { ".xpm", "image/x-xpm" },
80 { ".xsl", "text/xsl" },
81 { ".xhtml", "application/xhtml+xml" },
82 { ".wml", "text/vnd.wap.wml" },
83 { ".wmlc", "application/vnd.wap.wmlc" },
84 { "about:blank", nullptr },
85 { nullptr, nullptr }
86};
87
88static void setLight(QWidget *widget)
89{
90 // Make docs' contents visible in dark theme
91 QPalette p = widget->palette();
92 p.setColor(QPalette::Inactive, QPalette::Highlight,
93 p.color(QPalette::Active, QPalette::Highlight));
94 p.setColor(QPalette::Inactive, QPalette::HighlightedText,
95 p.color(QPalette::Active, QPalette::HighlightedText));
96 p.setColor(QPalette::Base, Qt::white);
97 p.setColor(QPalette::Text, Qt::black);
98 widget->setPalette(p);
99}
100
101static bool isDarkTheme()
102{
103 // Either Qt realizes that it is dark, or the palette exposes it, by having
104 // the window background darker than the text
105 return QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark
106 || QGuiApplication::palette().color(QPalette::Base).lightnessF()
107 < QGuiApplication::palette().color(QPalette::Text).lightnessF();
108}
109
110static void setPaletteFromApp(QWidget *widget)
111{
112 QPalette appPalette = QGuiApplication::palette();
113 // Detach the palette from the application palette, so it doesn't change directly when the
114 // application palette changes.
115 // That ensures that if the system is dark, and dark documentation is show, and the user
116 // switches the system to a light theme, that the documentation background stays dark for the
117 // visible page until either the we got informed by the change in application palette, or the
118 // user switched pages
119 appPalette.setColor(QPalette::Base, appPalette.color(QPalette::Base));
120 widget->setPalette(appPalette);
121}
122
123static QByteArray addDarkThemeToSvg(const QByteArray &data)
124{
125 // Parse the SVG file and attach an attribute to the svg element.
126 QXmlStreamReader reader(data);
127
128 // Assume that the first element is the svg element.
129 if (reader.readNextStartElement()) {
130 if (reader.name() == "svg") {
131 // The character offset refers to the character after the
132 // element start.
133 qint64 rest = reader.characterOffset();
134
135 // Write the svg element to a new byte array, adding an attribute
136 // if the theme is dark.
137 QByteArray newData;
138 QXmlStreamWriter writer(&newData);
139
140 writer.writeStartDocument();
141 writer.writeStartElement("svg");
142 writer.writeAttributes(reader.attributes());
143 writer.writeAttribute("class", "dark");
144
145 // Ensure that the opening tag for the svg element is closed.
146 writer.writeCharacters("");
147
148 // Return the new document start and the rest of the original,
149 // discarding the processing instructions before the svg element.
150 return newData + data.mid(rest);
151 }
152 }
153
154 // The entire file was read without finding an svg element, or an error
155 // occurred, so just return the original data.
156 return data;
157}
158
159static QByteArray getData(const QUrl &url, QWidget *widget)
160{
161 // This is a hack for Qt documentation,
162 // which decides to use a simpler CSS if the viewer does not have JavaScript
163 // which was a hack to decide if we are viewing in QTextBrowser or QtWebEngine et al.
164 // Force it to use the "normal" offline CSS even without JavaScript, since litehtml can
165 // handle that, and inject a dark themed CSS into Qt documentation for dark Qt Creator themes
166 QUrl actualUrl = url;
167 QString path = url.path(QUrl::FullyEncoded);
168 static const char simpleCss[] = "/offline-simple.css";
169 if (path.endsWith(simpleCss)) {
170 if (isDarkTheme()) {
171 // check if dark CSS is shipped with documentation
172 QString darkPath = path;
173 darkPath.replace(simpleCss, "/offline-dark.css");
174 actualUrl.setPath(darkPath);
175 QByteArray data = HelpEngineWrapper::instance().fileData(actualUrl);
176 if (!data.isEmpty()) {
177 // we found the dark style
178 // set background dark (by using app palette)
179 setPaletteFromApp(widget);
180 return data;
181 }
182 }
183 path.replace(simpleCss, "/offline.css");
184 actualUrl.setPath(path);
185 }
186
187 if (actualUrl.isValid()) {
188 const QByteArray data = HelpEngineWrapper::instance().fileData(actualUrl);
189 if (isDarkTheme() && path.endsWith(".svg"))
190 return addDarkThemeToSvg(data);
191 else
192 return data;
193 }
194
195 const bool isAbout = (actualUrl.toString() == "about:blank"_L1);
196 return isAbout ? HelpViewerImpl::AboutBlank.toUtf8()
197 : HelpViewerImpl::PageNotFoundMessage.arg(url.toString()).toUtf8();
198}
199
201{
202public:
204 {
205 QUrl url;
208 };
210 void setSourceInternal(const QUrl &url, int *vscroll = nullptr, bool reload = false);
211 void incrementZoom(int steps);
212 void applyZoom(int percentage);
213 void openInNewPage(const QUrl &url);
214 void showContextMenu(const QPoint &pos, const QUrl &link);
215
216 HelpViewer *q = nullptr;
220 int m_fontZoom = 100; // zoom percentage
222};
223
225{
226 return { m_viewer->url(), m_viewer->title(), m_viewer->verticalScrollBar()->value() };
227}
228
229void HelpViewerPrivate::setSourceInternal(const QUrl &url, int *vscroll, bool reload)
230{
231 QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
232
233 const bool isHelp = (url.toString() == "help"_L1);
234 const QUrl resolvedUrl = (isHelp ? QUrl(HelpViewerImpl::LocalHelpFile)
235 : HelpEngineWrapper::instance().findFile(url));
236
237 QUrl currentUrlWithoutFragment = m_viewer->url();
238 currentUrlWithoutFragment.setFragment({});
239 QUrl newUrlWithoutFragment = resolvedUrl;
240 newUrlWithoutFragment.setFragment({});
241
242 m_viewer->setUrl(resolvedUrl);
243 if (currentUrlWithoutFragment != newUrlWithoutFragment || reload) {
244 // Users can register arbitrary documentation, so we do not expect the documentation to
245 // support dark themes, and start with light palette.
246 // We override this if we find Qt's dark style
247 setLight(q);
248 QMimeDatabase mimeDb;
249 const QMimeType mimeType = mimeDb.mimeTypeForFile(url.fileName(),
250 QMimeDatabase::MatchExtension);
251 if (QImageReader::supportedMimeTypes().contains(mimeType.name().toUtf8()))
252 m_viewer->setHtml(QString("<img src=\"%1\"/>").arg(QString::fromUtf8(url.toEncoded())));
253 else
254 m_viewer->setHtml(QString::fromUtf8(getData(resolvedUrl, q)));
255
256 }
257 if (vscroll)
258 m_viewer->verticalScrollBar()->setValue(*vscroll);
259 else
260 m_viewer->scrollToAnchor(resolvedUrl.fragment(QUrl::FullyEncoded));
261
262 QGuiApplication::restoreOverrideCursor();
263
264 emit q->sourceChanged(q->source());
265 emit q->loadFinished();
266 emit q->titleChanged();
267}
268
270{
271 const int incrementPercentage = 10 * steps; // 10 percent increase by single step
272 const int previousZoom = m_fontZoom;
273 applyZoom(previousZoom + incrementPercentage);
274}
275
276void HelpViewerPrivate::applyZoom(int percentage)
277{
278 const int newZoom = qBound(10, percentage, 300);
279 if (newZoom == m_fontZoom)
280 return;
281 m_fontZoom = newZoom;
282 m_viewer->setZoomFactor(newZoom / 100.0);
283}
284
285void HelpViewerPrivate::openInNewPage(const QUrl &url)
286{
287 if (!url.isEmpty() && url.isValid())
288 OpenPagesManager::instance()->createPage(url);
289}
290
291void HelpViewerPrivate::showContextMenu(const QPoint &pos, const QUrl &link)
292{
293 QMenu menu;
294 if (!link.isEmpty() && link.isValid()) {
295 menu.addAction(HelpViewer::tr("Open Link"), q, [this, link] { q->setSource(link); });
296 menu.addAction(HelpViewer::tr("Open Link in New Tab\tCtrl+LMB"), q,
297 [this, link] { openInNewPage(link); });
298 if (kHasClipboard) {
299 menu.addAction(HelpViewer::tr("Copy &Link Location"), q,
300 [link] { setClipboardText(link.toString()); });
301 }
302 } else if (kHasClipboard && !q->selectedText().isEmpty()) {
303 menu.addAction(HelpViewer::tr("Copy"), q, [this] { setClipboardText(q->selectedText()); });
304 } else {
305 menu.addAction(HelpViewer::tr("Reload"), q, &HelpViewer::reload);
306 }
307 menu.exec(m_viewer->viewport()->mapToGlobal(pos));
308}
309
310HelpViewer::HelpViewer(qreal zoom, QWidget *parent)
311 : QWidget(parent)
312 , d(new HelpViewerPrivate)
313{
314 auto layout = new QVBoxLayout;
315 d->q = this;
316 d->m_viewer = new QLiteHtmlWidget(this);
317 d->m_viewer->setResourceHandler([this](const QUrl &url) { return getData(url, this); });
318 d->m_viewer->viewport()->installEventFilter(this);
319 const int zoomPercentage = zoom == 0 ? 100 : zoom * 100;
320 d->applyZoom(zoomPercentage);
321 connect(d->m_viewer, &QLiteHtmlWidget::linkClicked, this, [this](const QUrl &url) {
322 if (QGuiApplication::keyboardModifiers() & Qt::ControlModifier)
324 else
325 setSource(url);
326 });
327 connect(d->m_viewer, &QLiteHtmlWidget::linkHighlighted, this, [this](const QUrl &url) {
328 d->m_highlightedLink = url;
329 emit highlighted(url);
330 });
331 connect(d->m_viewer, &QLiteHtmlWidget::contextMenuRequested, this,
332 [this](const QPoint &pos, const QUrl &link) { d->showContextMenu(pos, link); });
333#if QT_CONFIG(clipboard)
334 connect(d->m_viewer, &QLiteHtmlWidget::copyAvailable, this, &HelpViewer::copyAvailable);
335#endif
336 setLayout(layout);
337 layout->setContentsMargins(0, 0, 0, 0);
338 layout->addWidget(d->m_viewer, 10);
339
340 // If the platform supports it, changes of color scheme light/dark take effect during runtime:
341 connect(
342 QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this,
343 [this] {
344 int vscroll = d->m_viewer->verticalScrollBar()->value();
345 d->setSourceInternal(source(), &vscroll, /*reload*/ true);
346 },
347 // Queue to make sure that the palette is actually applied on the application
348 Qt::QueuedConnection);
349}
350
352{
353 delete d;
354}
355
357{
358 return d->m_viewer->defaultFont();
359}
360
361void HelpViewer::setViewerFont(const QFont &font)
362{
363 d->m_viewer->setDefaultFont(font);
364}
365
367{
369}
370
372{
374}
375
377{
378 d->applyZoom(100);
379}
380
382{
383 return d->m_viewer->zoomFactor();
384}
385
387{
388 return d->m_viewer->title();
389}
390
391QUrl HelpViewer::source() const
392{
393 return d->m_viewer->url();
394}
395
397{
398 doSetSource(source(), true);
399}
400
401void HelpViewer::setSource(const QUrl &url)
402{
403 doSetSource(url, false);
404}
405
407{
408 d->m_viewer->setFocus();
409}
410
411void HelpViewer::doSetSource(const QUrl &url, bool reload)
412{
413 if (launchWithExternalApp(url))
414 return;
415
416 d->m_forwardItems.clear();
417 emit forwardAvailable(false);
418 if (d->m_viewer->url().isValid()) {
419 d->m_backItems.push_back(d->currentHistoryItem());
420 while (d->m_backItems.size() > kMaxHistoryItems) // this should trigger only once anyhow
421 d->m_backItems.erase(d->m_backItems.begin());
422 emit backwardAvailable(true);
423 }
424
425 d->setSourceInternal(url, nullptr, reload);
426}
427
428#if QT_CONFIG(printer)
429void HelpViewer::print(QPrinter *printer)
430{
431 d->m_viewer->print(printer);
432}
433#endif
434
436{
437 return d->m_viewer->selectedText();
438}
439
441{
442 return !d->m_forwardItems.empty();
443}
444
446{
447 return !d->m_backItems.empty();
448}
449
450static QTextDocument::FindFlags textDocumentFlagsForFindFlags(HelpViewer::FindFlags flags)
451{
452 QTextDocument::FindFlags textDocFlags;
453 if (flags & HelpViewer::FindBackward)
454 textDocFlags |= QTextDocument::FindBackward;
455 if (flags & HelpViewer::FindCaseSensitively)
456 textDocFlags |= QTextDocument::FindCaseSensitively;
457 return textDocFlags;
458}
459
460bool HelpViewer::findText(const QString &text, FindFlags flags, bool incremental, bool fromSearch)
461{
462 Q_UNUSED(fromSearch);
463 return d->m_viewer->findText(text, textDocumentFlagsForFindFlags(flags), incremental);
464}
465
466#if QT_CONFIG(clipboard)
467void HelpViewer::copy()
468{
469 QGuiApplication::clipboard()->setText(selectedText());
470}
471#endif
472
473void HelpViewer::home()
474{
475 setSource(QUrl(HelpEngineWrapper::instance().homePage()));
476}
477
479{
481 if (d->m_forwardItems.empty())
482 return;
483 d->m_backItems.push_back(nextItem);
484 nextItem = d->m_forwardItems.front();
485 d->m_forwardItems.erase(d->m_forwardItems.begin());
486
487 emit backwardAvailable(isBackwardAvailable());
488 emit forwardAvailable(isForwardAvailable());
489 d->setSourceInternal(nextItem.url, &nextItem.vscroll);
490}
491
493{
495 if (d->m_backItems.empty())
496 return;
497 d->m_forwardItems.insert(d->m_forwardItems.begin(), previousItem);
498 previousItem = d->m_backItems.back();
499 d->m_backItems.pop_back();
500
501 emit backwardAvailable(isBackwardAvailable());
502 emit forwardAvailable(isForwardAvailable());
503 d->setSourceInternal(previousItem.url, &previousItem.vscroll);
504}
505
506bool HelpViewer::eventFilter(QObject *src, QEvent *event)
507{
508 if (event->type() == QEvent::Wheel) {
509 auto we = static_cast<QWheelEvent *>(event);
510 if (we->modifiers() == Qt::ControlModifier) {
511 we->accept();
512 const int deltaY = we->angleDelta().y();
513 if (deltaY != 0)
514 d->incrementZoom(deltaY / 120);
515 return true;
516 }
517 } else if (event->type() == QEvent::MouseButtonRelease) {
518 // Ctrl+LMB is handled when the link is clicked, as litehtml only reacts on the left button.
519 auto me = static_cast<QMouseEvent *>(event);
520 if (me->button() == Qt::MiddleButton && !d->m_highlightedLink.isEmpty()
521 && d->m_highlightedLink.isValid()) {
522 me->accept();
523 d->openInNewPage(d->m_highlightedLink);
524 return true;
525 }
526 }
527 return QWidget::eventFilter(src, event);
528}
529
530bool HelpViewer::isLocalUrl(const QUrl &url)
531{
533 const QString &scheme = url.scheme();
534 return scheme.isEmpty()
535 || scheme == "file"_L1
536 || scheme == "qrc"_L1
537 || scheme == "data"_L1
538 || scheme == "qthelp"_L1
539 || scheme == "about"_L1;
540}
541
542bool HelpViewer::canOpenPage(const QString &path)
543{
545 return !mimeFromUrl(QUrl::fromLocalFile(path)).isEmpty();
546}
547
548QString HelpViewer::mimeFromUrl(const QUrl &url)
549{
551 const QString &path = url.path();
552 const int index = path.lastIndexOf(u'.');
553 const QByteArray &ext = path.mid(index).toUtf8().toLower();
554
555 const ExtensionMap *e = extensionMap;
556 while (e->extension) {
557 if (ext == e->extension)
558 return QLatin1StringView(e->mimeType);
559 ++e;
560 }
561 return "application/octet-stream"_L1;
562}
563
564bool HelpViewer::launchWithExternalApp(const QUrl &url)
565{
567 if (isLocalUrl(url)) {
569 const QUrl &resolvedUrl = helpEngine.findFile(url);
570 if (!resolvedUrl.isValid())
571 return false;
572
573 const QString& path = resolvedUrl.toLocalFile();
574 if (!canOpenPage(path)) {
575 QTemporaryFile tmpTmpFile;
576 if (!tmpTmpFile.open())
577 return false;
578
579 const QString &extension = QFileInfo(path).completeSuffix();
580 QFile actualTmpFile(tmpTmpFile.fileName() % "."_L1 % extension);
581 if (!actualTmpFile.open(QIODevice::ReadWrite | QIODevice::Truncate))
582 return false;
583
584 actualTmpFile.write(helpEngine.fileData(resolvedUrl));
585 actualTmpFile.close();
586 return QDesktopServices::openUrl(QUrl::fromLocalFile(actualTmpFile.fileName()));
587 }
588 return false;
589 }
590 return QDesktopServices::openUrl(url);
591}
592
593QT_END_NAMESPACE
static HelpEngineWrapper & instance()
HistoryItem currentHistoryItem() const
void openInNewPage(const QUrl &url)
void showContextMenu(const QPoint &pos, const QUrl &link)
std::vector< HistoryItem > m_backItems
std::vector< HistoryItem > m_forwardItems
void applyZoom(int percentage)
QLiteHtmlWidget * m_viewer
void incrementZoom(int steps)
void setSourceInternal(const QUrl &url, int *vscroll=nullptr, bool reload=false)
void setViewerFont(const QFont &font)
void scaleUp()
QString title() const
void resetScale()
QString selectedText() const
void forward()
void scaleDown()
void reload()
bool isForwardAvailable() const
void backward()
QFont viewerFont() const
~HelpViewer() override
QUrl source() const
bool findText(const QString &text, FindFlags flags, bool incremental, bool fromSearch)
qreal scale() const
bool isBackwardAvailable() const
bool eventFilter(QObject *src, QEvent *event) override
Filters events if this object has been installed as an event filter for the watched object.
void doSetFocus()
static OpenPagesManager * instance()
static void setPaletteFromApp(QWidget *widget)
static QByteArray addDarkThemeToSvg(const QByteArray &data)
static bool isDarkTheme()
static void setLight(QWidget *widget)
static QTextDocument::FindFlags textDocumentFlagsForFindFlags(HelpViewer::FindFlags flags)
static QByteArray getData(const QUrl &url, QWidget *widget)
const int kMaxHistoryItems
static void setClipboardText(const QString &text)
constexpr bool kHasClipboard
const char * mimeType
const char * extension
#define TRACE_OBJ
Definition tracer.h:34