11#include <QtCore/QFileInfo>
12#include <QtCore/QMimeDatabase>
13#include <QtCore/QStringBuilder>
14#include <QtCore/QTemporaryFile>
15#include <QtCore/QXmlStreamReader>
16#include <QtCore/QXmlStreamWriter>
18#include <QtGui/QDesktopServices>
19#if QT_CONFIG(clipboard)
20#include <QtGui/QClipboard>
22#include <QtGui/QGuiApplication>
23#include <QtGui/QImageReader>
24#include <QtGui/QStyleHints>
25#include <QtGui/QWheelEvent>
27#include <QtWidgets/QMenu>
28#include <QtWidgets/QScrollBar>
29#include <QtWidgets/QVBoxLayout>
31#include <QtHelp/QHelpEngineCore>
33#include <qlitehtmlwidget.h>
37using namespace Qt::StringLiterals;
44#if QT_CONFIG(clipboard)
45 QGuiApplication::clipboard()->setText(text);
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" },
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 },
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);
105 return QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark
106 || QGuiApplication::palette().color(QPalette::Base).lightnessF()
107 < QGuiApplication::palette().color(QPalette::Text).lightnessF();
112 QPalette appPalette = QGuiApplication::palette();
119 appPalette.setColor(QPalette::Base, appPalette.color(QPalette::Base));
120 widget->setPalette(appPalette);
126 QXmlStreamReader reader(data);
129 if (reader.readNextStartElement()) {
130 if (reader.name() ==
"svg") {
133 qint64 rest = reader.characterOffset();
138 QXmlStreamWriter writer(&newData);
140 writer.writeStartDocument();
141 writer.writeStartElement(
"svg");
142 writer.writeAttributes(reader.attributes());
143 writer.writeAttribute(
"class",
"dark");
146 writer.writeCharacters(
"");
150 return newData + data.mid(rest);
166 QUrl actualUrl = url;
167 QString path = url.path(QUrl::FullyEncoded);
168 static const char simpleCss[] =
"/offline-simple.css";
169 if (path.endsWith(simpleCss)) {
172 QString darkPath = path;
173 darkPath.replace(simpleCss,
"/offline-dark.css");
174 actualUrl.setPath(darkPath);
176 if (!data.isEmpty()) {
179 setPaletteFromApp(widget);
183 path.replace(simpleCss,
"/offline.css");
184 actualUrl.setPath(path);
187 if (actualUrl.isValid()) {
190 return addDarkThemeToSvg(data);
195 const bool isAbout = (actualUrl.toString() ==
"about:blank"_L1);
196 return isAbout ? HelpViewerImpl::AboutBlank.toUtf8()
197 : HelpViewerImpl::PageNotFoundMessage.arg(url.toString()).toUtf8();
226 return { m_viewer->url(), m_viewer->title(), m_viewer->verticalScrollBar()->value() };
231 QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
233 const bool isHelp = (url.toString() ==
"help"_L1);
234 const QUrl resolvedUrl = (isHelp ? QUrl(HelpViewerImpl::LocalHelpFile)
235 : HelpEngineWrapper::instance().findFile(url));
237 QUrl currentUrlWithoutFragment = m_viewer->url();
238 currentUrlWithoutFragment.setFragment({});
239 QUrl newUrlWithoutFragment = resolvedUrl;
240 newUrlWithoutFragment.setFragment({});
242 m_viewer->setUrl(resolvedUrl);
243 if (currentUrlWithoutFragment != newUrlWithoutFragment || reload) {
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())));
254 m_viewer->setHtml(QString::fromUtf8(getData(resolvedUrl, q)));
258 m_viewer->verticalScrollBar()->setValue(*vscroll);
260 m_viewer->scrollToAnchor(resolvedUrl.fragment(QUrl::FullyEncoded));
262 QGuiApplication::restoreOverrideCursor();
264 emit q->sourceChanged(q->source());
265 emit q->loadFinished();
266 emit q->titleChanged();
271 const int incrementPercentage = 10 * steps;
278 const int newZoom = qBound(10, percentage, 300);
282 m_viewer->setZoomFactor(newZoom / 100.0);
287 if (!url.isEmpty() && url.isValid())
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); });
299 menu.addAction(HelpViewer::tr(
"Copy &Link Location"),
q,
300 [link] { setClipboardText(link.toString()); });
303 menu.addAction(HelpViewer::tr(
"Copy"),
q, [
this] { setClipboardText(
q->selectedText()); });
307 menu.exec(m_viewer->viewport()->mapToGlobal(pos));
314 auto layout =
new QVBoxLayout;
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;
321 connect(d->m_viewer, &QLiteHtmlWidget::linkClicked,
this, [
this](
const QUrl &url) {
322 if (QGuiApplication::keyboardModifiers() & Qt::ControlModifier)
327 connect(d->m_viewer, &QLiteHtmlWidget::linkHighlighted,
this, [
this](
const QUrl &url) {
328 d->m_highlightedLink = url;
329 emit highlighted(url);
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);
337 layout->setContentsMargins(0, 0, 0, 0);
338 layout->addWidget(d->m_viewer, 10);
342 QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged,
this,
344 int vscroll = d->m_viewer->verticalScrollBar()->value();
348 Qt::QueuedConnection);
358 return d->m_viewer->defaultFont();
363 d->m_viewer->setDefaultFont(font);
383 return d->m_viewer->zoomFactor();
388 return d->m_viewer->title();
393 return d->m_viewer->url();
398 doSetSource(source(),
true);
403 doSetSource(url,
false);
408 d->m_viewer->setFocus();
411void HelpViewer::doSetSource(
const QUrl &url,
bool reload)
413 if (launchWithExternalApp(url))
416 d->m_forwardItems.clear();
417 emit forwardAvailable(
false);
418 if (d->m_viewer->url().isValid()) {
421 d->m_backItems.erase(d->m_backItems.begin());
422 emit backwardAvailable(
true);
428#if QT_CONFIG(printer)
429void HelpViewer::print(QPrinter *printer)
431 d->m_viewer->print(printer);
437 return d->m_viewer->selectedText();
442 return !d->m_forwardItems.empty();
447 return !d->m_backItems.empty();
452 QTextDocument::FindFlags textDocFlags;
453 if (flags & HelpViewer::FindBackward)
454 textDocFlags |= QTextDocument::FindBackward;
455 if (flags & HelpViewer::FindCaseSensitively)
456 textDocFlags |= QTextDocument::FindCaseSensitively;
462 Q_UNUSED(fromSearch);
463 return d->m_viewer->findText(text, textDocumentFlagsForFindFlags(flags), incremental);
466#if QT_CONFIG(clipboard)
467void HelpViewer::copy()
469 QGuiApplication::clipboard()->setText(selectedText());
475 setSource(QUrl(HelpEngineWrapper::instance().homePage()));
481 if (d->m_forwardItems.empty())
483 d->m_backItems.push_back(nextItem);
484 nextItem = d->m_forwardItems.front();
485 d->m_forwardItems.erase(d->m_forwardItems.begin());
495 if (d->m_backItems.empty())
497 d->m_forwardItems.insert(d->m_forwardItems.begin(), previousItem);
498 previousItem = d->m_backItems.back();
499 d->m_backItems.pop_back();
508 if (event->type() == QEvent::Wheel) {
509 auto we =
static_cast<QWheelEvent *>(event);
510 if (we->modifiers() == Qt::ControlModifier) {
512 const int deltaY = we->angleDelta().y();
517 }
else if (event->type() == QEvent::MouseButtonRelease) {
519 auto me =
static_cast<QMouseEvent *>(event);
520 if (me->button() == Qt::MiddleButton && !d->m_highlightedLink.isEmpty()
521 && d->m_highlightedLink.isValid()) {
527 return QWidget::eventFilter(src, event);
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;
545 return !mimeFromUrl(QUrl::fromLocalFile(path)).isEmpty();
551 const QString &path = url.path();
552 const int index = path.lastIndexOf(u'.');
553 const QByteArray &ext = path.mid(index).toUtf8().toLower();
561 return "application/octet-stream"_L1;
567 if (isLocalUrl(url)) {
569 const QUrl &resolvedUrl = helpEngine.findFile(url);
570 if (!resolvedUrl.isValid())
573 const QString& path = resolvedUrl.toLocalFile();
574 if (!canOpenPage(path)) {
575 QTemporaryFile tmpTmpFile;
576 if (!tmpTmpFile.open())
579 const QString &extension = QFileInfo(path).completeSuffix();
580 QFile actualTmpFile(tmpTmpFile.fileName() %
"."_L1 % extension);
581 if (!actualTmpFile.open(QIODevice::ReadWrite | QIODevice::Truncate))
584 actualTmpFile.write(helpEngine.fileData(resolvedUrl));
585 actualTmpFile.close();
586 return QDesktopServices::openUrl(QUrl::fromLocalFile(actualTmpFile.fileName()));
590 return QDesktopServices::openUrl(url);
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)
QString selectedText() const
bool isForwardAvailable() const
bool findText(const QString &text, FindFlags flags, bool incremental, bool fromSearch)
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 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