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/QWheelEvent>
20
21#include <QtWidgets/QScrollBar>
22#include <QtWidgets/QVBoxLayout>
23
24#include <QtHelp/QHelpEngineCore>
25
26#include <qlitehtmlwidget.h>
27
28QT_BEGIN_NAMESPACE
29
30using namespace Qt::StringLiterals;
31
32const int kMaxHistoryItems = 20;
33
34const struct ExtensionMap {
35 const char *extension;
36 const char *mimeType;
37} extensionMap[] = {
38 { ".bmp", "image/bmp" },
39 { ".css", "text/css" },
40 { ".gif", "image/gif" },
41 { ".html", "text/html" },
42 { ".htm", "text/html" },
43 { ".ico", "image/x-icon" },
44 { ".jpeg", "image/jpeg" },
45 { ".jpg", "image/jpeg" },
46 { ".js", "application/x-javascript" },
47 { ".mng", "video/x-mng" },
48 { ".pbm", "image/x-portable-bitmap" },
49 { ".pgm", "image/x-portable-graymap" },
50 { ".pdf", nullptr },
51 { ".png", "image/png" },
52 { ".ppm", "image/x-portable-pixmap" },
53 { ".rss", "application/rss+xml" },
54 { ".svg", "image/svg+xml" },
55 { ".svgz", "image/svg+xml" },
56 { ".text", "text/plain" },
57 { ".tif", "image/tiff" },
58 { ".tiff", "image/tiff" },
59 { ".txt", "text/plain" },
60 { ".xbm", "image/x-xbitmap" },
61 { ".xml", "text/xml" },
62 { ".xpm", "image/x-xpm" },
63 { ".xsl", "text/xsl" },
64 { ".xhtml", "application/xhtml+xml" },
65 { ".wml", "text/vnd.wap.wml" },
66 { ".wmlc", "application/vnd.wap.wmlc" },
67 { "about:blank", nullptr },
68 { nullptr, nullptr }
69};
70
71static QByteArray getData(const QUrl &url)
72{
73 // TODO: this is just a hack for Qt documentation
74 // which decides to use a simpler CSS if the viewer does not have JavaScript
75 // which was a hack to decide if we are viewing in QTextBrowser or QtWebEngine et al
76 QUrl actualUrl = url;
77 QString path = url.path(QUrl::FullyEncoded);
78 static const char simpleCss[] = "/offline-simple.css";
79 if (path.endsWith(simpleCss)) {
80 path.replace(simpleCss, "/offline.css");
81 actualUrl.setPath(path);
82 }
83
84 if (actualUrl.isValid())
85 return HelpEngineWrapper::instance().fileData(actualUrl);
86
87 const bool isAbout = (actualUrl.toString() == "about:blank"_L1);
88 return isAbout ? HelpViewerImpl::AboutBlank.toUtf8()
89 : HelpViewerImpl::PageNotFoundMessage.arg(url.toString()).toUtf8();
90}
91
93{
94public:
96 {
100 };
102 void setSourceInternal(const QUrl &url, int *vscroll = nullptr, bool reload = false);
103 void incrementZoom(int steps);
104 void applyZoom(int percentage);
105
106 HelpViewer *q = nullptr;
110 int m_fontZoom = 100; // zoom percentage
111};
112
114{
115 return { m_viewer->url(), m_viewer->title(), m_viewer->verticalScrollBar()->value() };
116}
117
118void HelpViewerPrivate::setSourceInternal(const QUrl &url, int *vscroll, bool reload)
119{
120 QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
121
122 const bool isHelp = (url.toString() == "help"_L1);
123 const QUrl resolvedUrl = (isHelp ? HelpViewerImpl::LocalHelpFile
124 : HelpEngineWrapper::instance().findFile(url));
125
126 QUrl currentUrlWithoutFragment = m_viewer->url();
127 currentUrlWithoutFragment.setFragment({});
128 QUrl newUrlWithoutFragment = resolvedUrl;
129 newUrlWithoutFragment.setFragment({});
130
131 m_viewer->setUrl(resolvedUrl);
132 if (currentUrlWithoutFragment != newUrlWithoutFragment || reload)
133 m_viewer->setHtml(QString::fromUtf8(getData(resolvedUrl)));
134 if (vscroll)
135 m_viewer->verticalScrollBar()->setValue(*vscroll);
136 else
137 m_viewer->scrollToAnchor(resolvedUrl.fragment(QUrl::FullyEncoded));
138
139 QGuiApplication::restoreOverrideCursor();
140
141 emit q->sourceChanged(q->source());
142 emit q->loadFinished();
143 emit q->titleChanged();
144}
145
147{
148 const int incrementPercentage = 10 * steps; // 10 percent increase by single step
149 const int previousZoom = m_fontZoom;
150 applyZoom(previousZoom + incrementPercentage);
151}
152
153void HelpViewerPrivate::applyZoom(int percentage)
154{
155 const int newZoom = qBound(10, percentage, 300);
156 if (newZoom == m_fontZoom)
157 return;
158 m_fontZoom = newZoom;
159 m_viewer->setZoomFactor(newZoom / 100.0);
160}
161
162HelpViewer::HelpViewer(qreal zoom, QWidget *parent)
163 : QWidget(parent)
164 , d(new HelpViewerPrivate)
165{
166 auto layout = new QVBoxLayout;
167 d->q = this;
168 d->m_viewer = new QLiteHtmlWidget(this);
169 d->m_viewer->setResourceHandler([](const QUrl &url) { return getData(url); });
170 d->m_viewer->viewport()->installEventFilter(this);
171 const int zoomPercentage = zoom == 0 ? 100 : zoom * 100;
172 d->applyZoom(zoomPercentage);
173 connect(d->m_viewer, &QLiteHtmlWidget::linkClicked, this, &HelpViewer::setSource);
174 connect(d->m_viewer, &QLiteHtmlWidget::linkHighlighted, this, &HelpViewer::highlighted);
175#if QT_CONFIG(clipboard)
176 connect(d->m_viewer, &QLiteHtmlWidget::copyAvailable, this, &HelpViewer::copyAvailable);
177#endif
178 setLayout(layout);
179 layout->setContentsMargins(0, 0, 0, 0);
180 layout->addWidget(d->m_viewer, 10);
181
182 // Make docs' contents visible in dark theme
183 QPalette p = palette();
184 p.setColor(QPalette::Inactive, QPalette::Highlight,
185 p.color(QPalette::Active, QPalette::Highlight));
186 p.setColor(QPalette::Inactive, QPalette::HighlightedText,
187 p.color(QPalette::Active, QPalette::HighlightedText));
188 p.setColor(QPalette::Base, Qt::white);
189 p.setColor(QPalette::Text, Qt::black);
190 setPalette(p);
191}
192
194{
195 delete d;
196}
197
199{
200 return d->m_viewer->defaultFont();
201}
202
203void HelpViewer::setViewerFont(const QFont &font)
204{
205 d->m_viewer->setDefaultFont(font);
206}
207
209{
211}
212
214{
216}
217
219{
220 d->applyZoom(100);
221}
222
224{
225 return d->m_viewer->zoomFactor();
226}
227
229{
230 return d->m_viewer->title();
231}
232
234{
235 return d->m_viewer->url();
236}
237
239{
240 doSetSource(source(), true);
241}
242
243void HelpViewer::setSource(const QUrl &url)
244{
245 doSetSource(url, false);
246}
247
248void HelpViewer::doSetSource(const QUrl &url, bool reload)
249{
250 if (launchWithExternalApp(url))
251 return;
252
253 d->m_forwardItems.clear();
254 emit forwardAvailable(false);
255 if (d->m_viewer->url().isValid()) {
256 d->m_backItems.push_back(d->currentHistoryItem());
257 while (d->m_backItems.size() > kMaxHistoryItems) // this should trigger only once anyhow
258 d->m_backItems.erase(d->m_backItems.begin());
259 emit backwardAvailable(true);
260 }
261
262 d->setSourceInternal(url, nullptr, reload);
263}
264
265#if QT_CONFIG(printer)
266void HelpViewer::print(QPrinter *printer)
267{
268 d->m_viewer->print(printer);
269}
270#endif
271
273{
274 return d->m_viewer->selectedText();
275}
276
278{
279 return !d->m_forwardItems.empty();
280}
281
283{
284 return !d->m_backItems.empty();
285}
286
287static QTextDocument::FindFlags textDocumentFlagsForFindFlags(HelpViewer::FindFlags flags)
288{
289 QTextDocument::FindFlags textDocFlags;
290 if (flags & HelpViewer::FindBackward)
291 textDocFlags |= QTextDocument::FindBackward;
292 if (flags & HelpViewer::FindCaseSensitively)
293 textDocFlags |= QTextDocument::FindCaseSensitively;
294 return textDocFlags;
295}
296
297bool HelpViewer::findText(const QString &text, FindFlags flags, bool incremental, bool fromSearch)
298{
299 Q_UNUSED(fromSearch);
300 return d->m_viewer->findText(text, textDocumentFlagsForFindFlags(flags), incremental);
301}
302
303#if QT_CONFIG(clipboard)
304void HelpViewer::copy()
305{
306 QGuiApplication::clipboard()->setText(selectedText());
307}
308#endif
309
310void HelpViewer::home()
311{
312 setSource(HelpEngineWrapper::instance().homePage());
313}
314
316{
318 if (d->m_forwardItems.empty())
319 return;
320 d->m_backItems.push_back(nextItem);
321 nextItem = d->m_forwardItems.front();
322 d->m_forwardItems.erase(d->m_forwardItems.begin());
323
324 emit backwardAvailable(isBackwardAvailable());
325 emit forwardAvailable(isForwardAvailable());
326 d->setSourceInternal(nextItem.url, &nextItem.vscroll);
327}
328
330{
332 if (d->m_backItems.empty())
333 return;
334 d->m_forwardItems.insert(d->m_forwardItems.begin(), previousItem);
335 previousItem = d->m_backItems.back();
336 d->m_backItems.pop_back();
337
338 emit backwardAvailable(isBackwardAvailable());
339 emit forwardAvailable(isForwardAvailable());
340 d->setSourceInternal(previousItem.url, &previousItem.vscroll);
341}
342
343bool HelpViewer::eventFilter(QObject *src, QEvent *event)
344{
345 if (event->type() == QEvent::Wheel) {
346 auto we = static_cast<QWheelEvent *>(event);
347 if (we->modifiers() == Qt::ControlModifier) {
348 we->accept();
349 const int deltaY = we->angleDelta().y();
350 if (deltaY != 0)
351 d->incrementZoom(deltaY / 120);
352 return true;
353 }
354 }
355 return QWidget::eventFilter(src, event);
356}
357
358bool HelpViewer::isLocalUrl(const QUrl &url)
359{
361 const QString &scheme = url.scheme();
362 return scheme.isEmpty()
363 || scheme == "file"_L1
364 || scheme == "qrc"_L1
365 || scheme == "data"_L1
366 || scheme == "qthelp"_L1
367 || scheme == "about"_L1;
368}
369
370bool HelpViewer::canOpenPage(const QString &path)
371{
373 return !mimeFromUrl(QUrl::fromLocalFile(path)).isEmpty();
374}
375
376QString HelpViewer::mimeFromUrl(const QUrl &url)
377{
379 const QString &path = url.path();
380 const int index = path.lastIndexOf(u'.');
381 const QByteArray &ext = path.mid(index).toUtf8().toLower();
382
383 const ExtensionMap *e = extensionMap;
384 while (e->extension) {
385 if (ext == e->extension)
386 return QLatin1StringView(e->mimeType);
387 ++e;
388 }
389 return "application/octet-stream"_L1;
390}
391
392bool HelpViewer::launchWithExternalApp(const QUrl &url)
393{
395 if (isLocalUrl(url)) {
397 const QUrl &resolvedUrl = helpEngine.findFile(url);
398 if (!resolvedUrl.isValid())
399 return false;
400
401 const QString& path = resolvedUrl.toLocalFile();
402 if (!canOpenPage(path)) {
403 QTemporaryFile tmpTmpFile;
404 if (!tmpTmpFile.open())
405 return false;
406
407 const QString &extension = QFileInfo(path).completeSuffix();
408 QFile actualTmpFile(tmpTmpFile.fileName() % "."_L1 % extension);
409 if (!actualTmpFile.open(QIODevice::ReadWrite | QIODevice::Truncate))
410 return false;
411
412 actualTmpFile.write(helpEngine.fileData(resolvedUrl));
413 actualTmpFile.close();
414 return QDesktopServices::openUrl(QUrl::fromLocalFile(actualTmpFile.fileName()));
415 }
416 return false;
417 }
418 return QDesktopServices::openUrl(url);
419}
420
421QT_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 QByteArray getData(const QUrl &url)
static QTextDocument::FindFlags textDocumentFlagsForFindFlags(HelpViewer::FindFlags flags)
const int kMaxHistoryItems
const char * mimeType
const char * extension
#define TRACE_OBJ
Definition tracer.h:34