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