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