aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/help
diff options
context:
space:
mode:
authorEike Ziller <eike.ziller@qt.io>2019-07-29 15:30:58 +0200
committerEike Ziller <eike.ziller@qt.io>2019-08-28 08:06:20 +0000
commit0efd65e07eb5090d50027ddec3d93960a7e075ac (patch)
tree1628743b3fe50199b0403cc4e3dab002c07d2249 /src/plugins/help
parent7855c9bb806d4d33561c713b9f7b1d3259995946 (diff)
Help: Add litehtml based viewer backend
For CMake add litehtml installation path to CMAKE_PREFIX_PATH For qmake pass litehtml installation path via LITEHTML_INSTALL_DIR qmake variable Release build of litehtml is recommended. The litehtml backend is used by default when available, you can force QTextBrowser again with the environment variable "QTC_HELPVIEWER_BACKEND=textbrowser". Some things are not implemented yet: - Text search - Context menu - Shift-drag to extend existing selection Change-Id: I79f989e5fe2063de2e9832abbed19b24d7a1a1fe Reviewed-by: hjk <hjk@qt.io>
Diffstat (limited to 'src/plugins/help')
-rw-r--r--src/plugins/help/CMakeLists.txt13
-rw-r--r--src/plugins/help/help.pro6
-rw-r--r--src/plugins/help/helpplugin.cpp12
-rw-r--r--src/plugins/help/litehtmlhelpviewer.cpp356
-rw-r--r--src/plugins/help/litehtmlhelpviewer.h99
-rw-r--r--src/plugins/help/qlitehtml/CMakeLists.txt19
-rw-r--r--src/plugins/help/qlitehtml/README.md24
-rw-r--r--src/plugins/help/qlitehtml/container_qpainter.cpp1086
-rw-r--r--src/plugins/help/qlitehtml/container_qpainter.h181
-rw-r--r--src/plugins/help/qlitehtml/qlitehtml.pri13
-rw-r--r--src/plugins/help/qlitehtml/qlitehtmlwidget.cpp574
-rw-r--r--src/plugins/help/qlitehtml/qlitehtmlwidget.h74
12 files changed, 2455 insertions, 2 deletions
diff --git a/src/plugins/help/CMakeLists.txt b/src/plugins/help/CMakeLists.txt
index e96f15d91c..72c81e789e 100644
--- a/src/plugins/help/CMakeLists.txt
+++ b/src/plugins/help/CMakeLists.txt
@@ -47,3 +47,16 @@ extend_qtc_plugin(Help
webenginehelpviewer.cpp
webenginehelpviewer.h
)
+
+find_package(litehtml QUIET)
+if (TARGET litehtml)
+ add_subdirectory(qlitehtml)
+ extend_qtc_plugin(Help
+ CONDITION TARGET qlitehtml
+ DEPENDS qlitehtml
+ DEFINES QTC_LITEHTML_HELPVIEWER
+ SOURCES
+ litehtmlhelpviewer.cpp
+ litehtmlhelpviewer.h
+ )
+endif()
diff --git a/src/plugins/help/help.pro b/src/plugins/help/help.pro
index 76c6db63a2..5d9cf34078 100644
--- a/src/plugins/help/help.pro
+++ b/src/plugins/help/help.pro
@@ -78,6 +78,12 @@ osx {
}
}
+!isEmpty(LITEHTML_INSTALL_DIR) {
+ include(qlitehtml/qlitehtml.pri)
+ HEADERS += litehtmlhelpviewer.h
+ SOURCES += litehtmlhelpviewer.cpp
+ DEFINES += QTC_LITEHTML_HELPVIEWER
+}
RESOURCES += help.qrc
include(../../shared/help/help.pri)
diff --git a/src/plugins/help/helpplugin.cpp b/src/plugins/help/helpplugin.cpp
index c06619f3bc..a459246890 100644
--- a/src/plugins/help/helpplugin.cpp
+++ b/src/plugins/help/helpplugin.cpp
@@ -46,6 +46,9 @@
#include "textbrowserhelpviewer.h"
#include "topicchooser.h"
+#ifdef QTC_LITEHTML_HELPVIEWER
+#include "litehtmlhelpviewer.h"
+#endif
#ifdef QTC_MAC_NATIVE_HELPVIEWER
#include "macwebkithelpviewer.h"
#endif
@@ -455,10 +458,15 @@ HelpViewer *HelpPlugin::createHelpViewer(qreal zoom)
using ViewerFactory = std::function<HelpViewer *()>;
using ViewerFactoryItem = QPair<QByteArray, ViewerFactory>; // id -> factory
QVector<ViewerFactoryItem> factories;
+#ifdef QTC_LITEHTML_HELPVIEWER
+ factories.append(qMakePair(QByteArray("litehtml"), [] { return new LiteHtmlHelpViewer(); }));
+#endif
#ifdef QTC_WEBENGINE_HELPVIEWER
- factories.append(qMakePair(QByteArray("qtwebengine"), []() { return new WebEngineHelpViewer(); }));
+ factories.append(
+ qMakePair(QByteArray("qtwebengine"), []() { return new WebEngineHelpViewer(); }));
#endif
- factories.append(qMakePair(QByteArray("textbrowser"), []() { return new TextBrowserHelpViewer(); }));
+ factories.append(
+ qMakePair(QByteArray("textbrowser"), []() { return new TextBrowserHelpViewer(); }));
#ifdef QTC_MAC_NATIVE_HELPVIEWER
// default setting
diff --git a/src/plugins/help/litehtmlhelpviewer.cpp b/src/plugins/help/litehtmlhelpviewer.cpp
new file mode 100644
index 0000000000..469188a7de
--- /dev/null
+++ b/src/plugins/help/litehtmlhelpviewer.cpp
@@ -0,0 +1,356 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "litehtmlhelpviewer.h"
+
+#include "localhelpmanager.h"
+
+#include <utils/algorithm.h>
+#include <utils/qtcassert.h>
+
+#include <QClipboard>
+#include <QGuiApplication>
+#include <QScrollBar>
+#include <QTimer>
+#include <QVBoxLayout>
+
+#include <QDebug>
+
+using namespace Help;
+using namespace Help::Internal;
+
+const int kMaxHistoryItems = 20;
+
+static QByteArray getData(const QUrl &url)
+{
+ // TODO: this is just a hack for Qt documentation
+ // which decides to use a simpler CSS if the viewer does not have JavaScript
+ // which was a hack to decide if we are viewing in QTextBrowser or QtWebEngine et al
+ QUrl actualUrl = url;
+ QString path = url.path(QUrl::FullyEncoded);
+ static const char simpleCss[] = "/offline-simple.css";
+ if (path.endsWith(simpleCss)) {
+ path.replace(simpleCss, "/offline.css");
+ actualUrl.setPath(path);
+ }
+ const LocalHelpManager::HelpData help = LocalHelpManager::helpData(actualUrl);
+
+ // TODO: this is a hack around for https://github.com/litehtml/litehtml/issues/91
+ QByteArray data = help.data;
+ if (actualUrl.path(QUrl::FullyEncoded).endsWith(".css"))
+ data.replace("inline-table", "inline");
+
+ return data;
+}
+
+LiteHtmlHelpViewer::LiteHtmlHelpViewer(QWidget *parent)
+ : HelpViewer(parent)
+ , m_viewer(new QLiteHtmlWidget)
+{
+ m_viewer->setResourceHandler([](const QUrl &url) { return getData(url); });
+ connect(m_viewer, &QLiteHtmlWidget::linkClicked, this, &LiteHtmlHelpViewer::setSource);
+ auto layout = new QVBoxLayout;
+ setLayout(layout);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(m_viewer, 10);
+ setFocusProxy(m_viewer);
+ QPalette p = palette();
+ p.setColor(QPalette::Inactive, QPalette::Highlight,
+ p.color(QPalette::Active, QPalette::Highlight));
+ p.setColor(QPalette::Inactive, QPalette::HighlightedText,
+ p.color(QPalette::Active, QPalette::HighlightedText));
+ p.setColor(QPalette::Base, Qt::white);
+ p.setColor(QPalette::Text, Qt::black);
+ setPalette(p);
+}
+
+LiteHtmlHelpViewer::~LiteHtmlHelpViewer() = default;
+
+QFont LiteHtmlHelpViewer::viewerFont() const
+{
+ return m_viewer->defaultFont();
+}
+
+void LiteHtmlHelpViewer::setViewerFont(const QFont &newFont)
+{
+ m_viewer->setDefaultFont(newFont);
+}
+
+void LiteHtmlHelpViewer::scaleUp()
+{
+ // TODO
+}
+
+void LiteHtmlHelpViewer::scaleDown()
+{
+ // TODO
+}
+
+void LiteHtmlHelpViewer::resetScale()
+{
+ // TODO
+}
+
+qreal LiteHtmlHelpViewer::scale() const
+{
+ // TODO
+ return 1;
+}
+
+void LiteHtmlHelpViewer::setScale(qreal scale)
+{
+ // TODO
+}
+
+QString LiteHtmlHelpViewer::title() const
+{
+ return m_viewer->title();
+}
+
+QUrl LiteHtmlHelpViewer::source() const
+{
+ return m_viewer->url();
+}
+
+void LiteHtmlHelpViewer::setSource(const QUrl &url)
+{
+ if (launchWithExternalApp(url))
+ return;
+ m_forwardItems.clear();
+ emit forwardAvailable(false);
+ if (m_viewer->url().isValid()) {
+ m_backItems.push_back(currentHistoryItem());
+ while (m_backItems.size() > kMaxHistoryItems) // this should trigger only once anyhow
+ m_backItems.erase(m_backItems.begin());
+ emit backwardAvailable(true);
+ }
+ setSourceInternal(url);
+}
+
+void LiteHtmlHelpViewer::setHtml(const QString &html)
+{
+ m_viewer->setUrl({"about:invalid"});
+ m_viewer->setHtml(html);
+}
+
+QString LiteHtmlHelpViewer::selectedText() const
+{
+ return m_viewer->selectedText();
+}
+
+bool LiteHtmlHelpViewer::isForwardAvailable() const
+{
+ return !m_forwardItems.empty();
+}
+
+bool LiteHtmlHelpViewer::isBackwardAvailable() const
+{
+ return !m_backItems.empty();
+}
+
+void LiteHtmlHelpViewer::addBackHistoryItems(QMenu *backMenu)
+{
+ int backCount = 0;
+ Utils::reverseForeach(m_backItems, [this, backMenu, &backCount](const HistoryItem &item) {
+ ++backCount;
+ auto action = new QAction(backMenu);
+ action->setText(item.title);
+ connect(action, &QAction::triggered, this, [this, backCount] { goBackward(backCount); });
+ backMenu->addAction(action);
+ });
+}
+
+void LiteHtmlHelpViewer::addForwardHistoryItems(QMenu *forwardMenu)
+{
+ int forwardCount = 0;
+ for (const HistoryItem &item : m_forwardItems) {
+ ++forwardCount;
+ auto action = new QAction(forwardMenu);
+ action->setText(item.title);
+ connect(action, &QAction::triggered, this, [this, forwardCount] {
+ goForward(forwardCount);
+ });
+ forwardMenu->addAction(action);
+ }
+}
+
+bool LiteHtmlHelpViewer::findText(
+ const QString &text, Core::FindFlags flags, bool incremental, bool fromSearch, bool *wrapped)
+{
+ // TODO
+ return false;
+}
+
+void LiteHtmlHelpViewer::copy()
+{
+ QGuiApplication::clipboard()->setText(selectedText());
+}
+
+void LiteHtmlHelpViewer::stop() {}
+
+void LiteHtmlHelpViewer::forward()
+{
+ goForward(1);
+}
+
+void LiteHtmlHelpViewer::backward()
+{
+ goBackward(1);
+}
+void LiteHtmlHelpViewer::goForward(int count)
+{
+ HistoryItem nextItem = currentHistoryItem();
+ for (int i = 0; i < count; ++i) {
+ QTC_ASSERT(!m_forwardItems.empty(), return );
+ m_backItems.push_back(nextItem);
+ nextItem = m_forwardItems.front();
+ m_forwardItems.erase(m_forwardItems.begin());
+ }
+ emit backwardAvailable(isBackwardAvailable());
+ emit forwardAvailable(isForwardAvailable());
+ setSourceInternal(nextItem.url, nextItem.vscroll);
+}
+
+void LiteHtmlHelpViewer::goBackward(int count)
+{
+ HistoryItem previousItem = currentHistoryItem();
+ for (int i = 0; i < count; ++i) {
+ QTC_ASSERT(!m_backItems.empty(), return );
+ m_forwardItems.insert(m_forwardItems.begin(), previousItem);
+ previousItem = m_backItems.back();
+ m_backItems.pop_back();
+ }
+ emit backwardAvailable(isBackwardAvailable());
+ emit forwardAvailable(isForwardAvailable());
+ setSourceInternal(previousItem.url, previousItem.vscroll);
+}
+
+void LiteHtmlHelpViewer::print(QPrinter *printer)
+{
+ // TODO
+}
+
+void LiteHtmlHelpViewer::setSourceInternal(const QUrl &url, Utils::optional<int> vscroll)
+{
+ slotLoadStarted();
+ QUrl currentUrlWithoutFragment = m_viewer->url();
+ currentUrlWithoutFragment.setFragment({});
+ QUrl newUrlWithoutFragment = url;
+ newUrlWithoutFragment.setFragment({});
+ m_viewer->setUrl(url);
+ if (currentUrlWithoutFragment != newUrlWithoutFragment)
+ m_viewer->setHtml(QString::fromUtf8(getData(url)));
+ if (vscroll)
+ m_viewer->verticalScrollBar()->setValue(*vscroll);
+ else
+ m_viewer->scrollToAnchor(url.fragment(QUrl::FullyEncoded));
+ slotLoadFinished();
+ emit titleChanged();
+}
+
+LiteHtmlHelpViewer::HistoryItem LiteHtmlHelpViewer::currentHistoryItem() const
+{
+ return {m_viewer->url(), m_viewer->title(), m_viewer->verticalScrollBar()->value()};
+}
+
+// -- private
+//void TextBrowserHelpWidget::contextMenuEvent(QContextMenuEvent *event)
+//{
+// QMenu menu("", nullptr);
+
+// QAction *copyAnchorAction = nullptr;
+// const QUrl link(linkAt(event->pos()));
+// if (!link.isEmpty() && link.isValid()) {
+// QAction *action = menu.addAction(tr("Open Link"));
+// connect(action, &QAction::triggered, this, [this, link]() {
+// setSource(link);
+// });
+// if (m_parent->isActionVisible(HelpViewer::Action::NewPage)) {
+// action = menu.addAction(QCoreApplication::translate("HelpViewer", Constants::TR_OPEN_LINK_AS_NEW_PAGE));
+// connect(action, &QAction::triggered, this, [this, link]() {
+// emit m_parent->newPageRequested(link);
+// });
+// }
+// if (m_parent->isActionVisible(HelpViewer::Action::ExternalWindow)) {
+// action = menu.addAction(QCoreApplication::translate("HelpViewer", Constants::TR_OPEN_LINK_IN_WINDOW));
+// connect(action, &QAction::triggered, this, [this, link]() {
+// emit m_parent->externalPageRequested(link);
+// });
+// }
+// copyAnchorAction = menu.addAction(tr("Copy Link"));
+// } else if (!textCursor().selectedText().isEmpty()) {
+// connect(menu.addAction(tr("Copy")), &QAction::triggered, this, &QTextEdit::copy);
+// } else {
+// connect(menu.addAction(tr("Reload")), &QAction::triggered, this, &QTextBrowser::reload);
+// }
+
+// if (copyAnchorAction == menu.exec(event->globalPos()))
+// QApplication::clipboard()->setText(link.toString());
+//}
+
+//bool TextBrowserHelpWidget::eventFilter(QObject *obj, QEvent *event)
+//{
+// if (obj == this) {
+// if (event->type() == QEvent::FontChange) {
+// if (!forceFont)
+// return true;
+// } else if (event->type() == QEvent::KeyPress) {
+// auto keyEvent = static_cast<QKeyEvent *>(event);
+// if (keyEvent->key() == Qt::Key_Slash) {
+// keyEvent->accept();
+// Core::Find::openFindToolBar(Core::Find::FindForwardDirection);
+// return true;
+// }
+// } else if (event->type() == QEvent::ToolTip) {
+// auto e = static_cast<const QHelpEvent *>(event);
+// QToolTip::showText(e->globalPos(), linkAt(e->pos()));
+// return true;
+// }
+// }
+// return QTextBrowser::eventFilter(obj, event);
+//}
+
+//void TextBrowserHelpWidget::mousePressEvent(QMouseEvent *e)
+//{
+// if (Utils::HostOsInfo::isLinuxHost() && m_parent->handleForwardBackwardMouseButtons(e))
+// return;
+// QTextBrowser::mousePressEvent(e);
+//}
+
+//void TextBrowserHelpWidget::mouseReleaseEvent(QMouseEvent *e)
+//{
+// if (!Utils::HostOsInfo::isLinuxHost() && m_parent->handleForwardBackwardMouseButtons(e))
+// return;
+
+// bool controlPressed = e->modifiers() & Qt::ControlModifier;
+// const QString link = linkAt(e->pos());
+// if (m_parent->isActionVisible(HelpViewer::Action::NewPage)
+// && (controlPressed || e->button() == Qt::MidButton) && !link.isEmpty()) {
+// emit m_parent->newPageRequested(QUrl(link));
+// return;
+// }
+
+// QTextBrowser::mouseReleaseEvent(e);
+//}
diff --git a/src/plugins/help/litehtmlhelpviewer.h b/src/plugins/help/litehtmlhelpviewer.h
new file mode 100644
index 0000000000..39d990f469
--- /dev/null
+++ b/src/plugins/help/litehtmlhelpviewer.h
@@ -0,0 +1,99 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Creator.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include "centralwidget.h"
+#include "helpviewer.h"
+#include "openpagesmanager.h"
+
+#include <utils/optional.h>
+
+#include <qlitehtmlwidget.h>
+
+#include <QTextBrowser>
+
+namespace Help {
+namespace Internal {
+
+class LiteHtmlHelpViewer : public HelpViewer
+{
+ Q_OBJECT
+
+public:
+ explicit LiteHtmlHelpViewer(QWidget *parent = nullptr);
+ ~LiteHtmlHelpViewer() override;
+
+ QFont viewerFont() const override;
+ void setViewerFont(const QFont &font) override;
+
+ qreal scale() const override;
+ void setScale(qreal scale) override;
+
+ QString title() const override;
+
+ QUrl source() const override;
+ void setSource(const QUrl &url) override;
+
+ void setHtml(const QString &html) override;
+
+ QString selectedText() const override;
+ bool isForwardAvailable() const override;
+ bool isBackwardAvailable() const override;
+ void addBackHistoryItems(QMenu *backMenu) override;
+ void addForwardHistoryItems(QMenu *forwardMenu) override;
+
+ bool findText(const QString &text, Core::FindFlags flags,
+ bool incremental, bool fromSearch, bool *wrapped = nullptr) override;
+
+ void scaleUp() override;
+ void scaleDown() override;
+ void resetScale() override;
+ void copy() override;
+ void stop() override;
+ void forward() override;
+ void backward() override;
+ void print(QPrinter *printer) override;
+
+private:
+ void goForward(int count);
+ void goBackward(int count);
+ void setSourceInternal(const QUrl &url, Utils::optional<int> vscroll = Utils::nullopt);
+
+ struct HistoryItem
+ {
+ QUrl url;
+ QString title;
+ int vscroll;
+ };
+ HistoryItem currentHistoryItem() const;
+
+ QLiteHtmlWidget *m_viewer;
+ std::vector<HistoryItem> m_backItems;
+ std::vector<HistoryItem> m_forwardItems;
+};
+
+} // namespace Internal
+} // namespace Help
diff --git a/src/plugins/help/qlitehtml/CMakeLists.txt b/src/plugins/help/qlitehtml/CMakeLists.txt
new file mode 100644
index 0000000000..d6174c56df
--- /dev/null
+++ b/src/plugins/help/qlitehtml/CMakeLists.txt
@@ -0,0 +1,19 @@
+cmake_minimum_required(VERSION 3.9)
+
+project(QLiteHtml)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_CXX_STANDARD 14)
+
+find_package(litehtml REQUIRED)
+find_package(Qt5 COMPONENTS Widgets REQUIRED)
+
+add_library(qlitehtml STATIC
+ container_qpainter.cpp container_qpainter.h
+ qlitehtmlwidget.cpp qlitehtmlwidget.h
+)
+
+target_include_directories(qlitehtml PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_link_libraries(qlitehtml PRIVATE Qt5::Widgets litehtml)
diff --git a/src/plugins/help/qlitehtml/README.md b/src/plugins/help/qlitehtml/README.md
new file mode 100644
index 0000000000..9a3cefcd9e
--- /dev/null
+++ b/src/plugins/help/qlitehtml/README.md
@@ -0,0 +1,24 @@
+# Qt backend for litehtml
+
+Provides
+
+* A QPainter based rendering backend for the light-weight HTML/CSS rendering engine [litehtml].
+* A QWidget that uses the QPainter based backend and provides API for simply setting the HTML text
+ and a base URL plus hook that are used for requesting referenced resources.
+
+## How to build
+
+Build and install [litehtml]. It is recommended to build [litehtml] in release mode
+
+```
+cd litehtml
+mkdir build
+cd build
+cmake -DCMAKE_INSTALL_PREFIX="$PWD/../install" -DCMAKE_BUILD_TYPE=Release -G Ninja ..
+cmake --build .
+cmake --install .
+```
+
+Add the [litehtml] installation path to the `CMAKE_PREFIX_PATH` when building the Qt backend
+
+[litehtml]: https://github.com/litehtml/litehtml
diff --git a/src/plugins/help/qlitehtml/container_qpainter.cpp b/src/plugins/help/qlitehtml/container_qpainter.cpp
new file mode 100644
index 0000000000..c99199881f
--- /dev/null
+++ b/src/plugins/help/qlitehtml/container_qpainter.cpp
@@ -0,0 +1,1086 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "container_qpainter.h"
+
+#include <QCursor>
+#include <QDebug>
+#include <QDir>
+#include <QFont>
+#include <QFontDatabase>
+#include <QFontMetrics>
+#include <QGuiApplication>
+#include <QLoggingCategory>
+#include <QPainter>
+#include <QPalette>
+#include <QScreen>
+#include <QTextLayout>
+#include <QUrl>
+
+#include <algorithm>
+
+const int kDragDistance = 5;
+
+using Font = QFont;
+using Context = QPainter;
+
+namespace {
+Q_LOGGING_CATEGORY(log, "qlitehtml", QtCriticalMsg)
+}
+
+static QFont toQFont(litehtml::uint_ptr hFont)
+{
+ return *reinterpret_cast<Font *>(hFont);
+}
+
+static QPainter *toQPainter(litehtml::uint_ptr hdc)
+{
+ return reinterpret_cast<Context *>(hdc);
+}
+
+static QRect toQRect(litehtml::position position)
+{
+ return {position.x, position.y, position.width, position.height};
+}
+
+static litehtml::elements_vector path(const litehtml::element::ptr &element)
+{
+ litehtml::elements_vector result;
+ litehtml::element::ptr current = element;
+ while (current) {
+ result.push_back(current);
+ current = current->parent();
+ }
+ std::reverse(std::begin(result), std::end(result));
+ return result;
+}
+
+static std::pair<litehtml::element::ptr, size_t> getCommonParent(const litehtml::elements_vector &a,
+ const litehtml::elements_vector &b)
+{
+ litehtml::element::ptr parent;
+ const size_t minSize = std::min(a.size(), b.size());
+ for (size_t i = 0; i < minSize; ++i) {
+ if (a.at(i) != b.at(i))
+ return {parent, i};
+ parent = a.at(i);
+ }
+ return {parent, minSize};
+}
+
+static std::pair<Selection::Element, Selection::Element> getStartAndEnd(const Selection::Element &a,
+ const Selection::Element &b)
+{
+ if (a.element == b.element) {
+ if (a.index <= b.index)
+ return {a, b};
+ return {b, a};
+ }
+ const litehtml::elements_vector aPath = path(a.element);
+ const litehtml::elements_vector bPath = path(b.element);
+ litehtml::element::ptr commonParent;
+ size_t firstDifferentIndex;
+ std::tie(commonParent, firstDifferentIndex) = getCommonParent(aPath, bPath);
+ if (!commonParent) {
+ qWarning() << "internal error: litehtml elements do not have common parent";
+ return {a, b};
+ }
+ if (commonParent == a.element)
+ return {a, a}; // 'a' already contains 'b'
+ if (commonParent == b.element)
+ return {b, b};
+ // find out if a or b is first in the child sub-trees of commonParent
+ const litehtml::element::ptr aBranch = aPath.at(firstDifferentIndex);
+ const litehtml::element::ptr bBranch = bPath.at(firstDifferentIndex);
+ for (int i = 0; i < int(commonParent->get_children_count()); ++i) {
+ const litehtml::element::ptr child = commonParent->get_child(i);
+ if (child == aBranch)
+ return {a, b};
+ if (child == bBranch)
+ return {b, a};
+ }
+ qWarning() << "internal error: failed to find out order of litehtml elements";
+ return {a, b};
+}
+
+static int findChild(const litehtml::element::ptr &child, const litehtml::element::ptr &parent)
+{
+ for (int i = 0; i < int(parent->get_children_count()); ++i)
+ if (parent->get_child(i) == child)
+ return i;
+ return -1;
+}
+
+static litehtml::element::ptr nextLeaf(const litehtml::element::ptr &element,
+ const litehtml::element::ptr &stop)
+{
+ if (element == stop)
+ return element;
+ litehtml::element::ptr current = element;
+ if (current->have_parent()) {
+ // find next sibling
+ const litehtml::element::ptr parent = current->parent();
+ const int childIndex = findChild(current, parent);
+ if (childIndex < 0) {
+ qWarning() << "internal error: filed to find litehtml child element in parent";
+ return stop;
+ }
+ if (childIndex + 1 >= int(parent->get_children_count())) // no sibling, move up
+ return nextLeaf(parent, stop);
+ current = parent->get_child(childIndex + 1);
+ }
+ // move down to first leaf
+ while (current != stop && current->get_children_count() > 0)
+ current = current->get_child(0);
+ return current;
+}
+
+static Selection::Element selectionDetails(const litehtml::element::ptr &element,
+ const QString &text,
+ const QPoint &pos)
+{
+ // shortcut, which _might_ not really be correct
+ if (element->get_children_count() > 0)
+ return {element, -1, -1}; // everything selected
+ const QFont &font = toQFont(element->get_font());
+ const QFontMetrics fm(font);
+ int previous = 0;
+ for (int i = 0; i < text.size(); ++i) {
+ const int width = fm.size(0, text.left(i + 1)).width();
+ if ((width + previous) / 2 >= pos.x())
+ return {element, i, previous};
+ previous = width;
+ }
+ return {element, text.size(), previous};
+}
+
+static Selection::Element deepest_child_at_point(const litehtml::document::ptr &document,
+ const QPoint &pos,
+ const QPoint &viewportPos,
+ Selection::Mode mode)
+{
+ if (!document)
+ return {};
+
+ // the following does not find the "smallest" element, it often consists of children
+ // with individual words as text...
+ const litehtml::element::ptr element = document->root()->get_element_by_point(pos.x(),
+ pos.y(),
+ viewportPos.x(),
+ viewportPos.y());
+ // ...so try to find a better match
+ const std::function<Selection::Element(litehtml::element::ptr, QRect)> recursion =
+ [&recursion, pos, mode](const litehtml::element::ptr &element,
+ const QRect &placement) -> Selection::Element {
+ if (!element)
+ return {};
+ Selection::Element result;
+ for (int i = 0; i < int(element->get_children_count()); ++i) {
+ const litehtml::element::ptr child = element->get_child(i);
+ result = recursion(child,
+ toQRect(child->get_position()).translated(placement.topLeft()));
+ if (result.element)
+ return result;
+ }
+ if (placement.contains(pos)) {
+ litehtml::tstring text;
+ element->get_text(text);
+ if (!text.empty()) {
+ return mode == Selection::Mode::Free
+ ? selectionDetails(element,
+ QString::fromStdString(text),
+ pos - placement.topLeft())
+ : Selection::Element({element, -1, -1});
+ }
+ }
+ return {};
+ };
+ return recursion(element, element ? toQRect(element->get_placement()) : QRect());
+}
+
+// CSS: 400 == normal, 700 == bold.
+// Qt: 50 == normal, 75 == bold
+static int cssWeightToQtWeight(int cssWeight)
+{
+ if (cssWeight <= 400)
+ return cssWeight * 50 / 400;
+ if (cssWeight >= 700)
+ return 75 + (cssWeight - 700) * 25 / 300;
+ return 50 + (cssWeight - 400) * 25 / 300;
+}
+
+static QFont::Style toQFontStyle(litehtml::font_style style)
+{
+ switch (style) {
+ case litehtml::fontStyleNormal:
+ return QFont::StyleNormal;
+ case litehtml::fontStyleItalic:
+ return QFont::StyleItalic;
+ }
+ // should not happen
+ qWarning(log) << "Unknown litehtml font style:" << style;
+ return QFont::StyleNormal;
+}
+
+static QColor toQColor(const litehtml::web_color &color)
+{
+ return {color.red, color.green, color.blue, color.alpha};
+}
+
+static Qt::PenStyle borderPenStyle(litehtml::border_style style)
+{
+ switch (style) {
+ case litehtml::border_style_dotted:
+ return Qt::DotLine;
+ case litehtml::border_style_dashed:
+ return Qt::DashLine;
+ case litehtml::border_style_solid:
+ return Qt::SolidLine;
+ default:
+ qWarning(log) << "Unsupported border style:" << style;
+ }
+ return Qt::SolidLine;
+}
+
+static QPen borderPen(const litehtml::border &border)
+{
+ return {toQColor(border.color), qreal(border.width), borderPenStyle(border.style)};
+}
+
+static QCursor toQCursor(const QString &c)
+{
+ if (c == "alias")
+ return {Qt::PointingHandCursor}; // ???
+ if (c == "all-scroll")
+ return {Qt::SizeAllCursor};
+ if (c == "auto")
+ return {Qt::ArrowCursor}; // ???
+ if (c == "cell")
+ return {Qt::UpArrowCursor};
+ if (c == "context-menu")
+ return {Qt::ArrowCursor}; // ???
+ if (c == "col-resize")
+ return {Qt::SplitHCursor};
+ if (c == "copy")
+ return {Qt::DragCopyCursor};
+ if (c == "crosshair")
+ return {Qt::CrossCursor};
+ if (c == "default")
+ return {Qt::ArrowCursor};
+ if (c == "e-resize")
+ return {Qt::SizeHorCursor}; // ???
+ if (c == "ew-resize")
+ return {Qt::SizeHorCursor};
+ if (c == "grab")
+ return {Qt::OpenHandCursor};
+ if (c == "grabbing")
+ return {Qt::ClosedHandCursor};
+ if (c == "help")
+ return {Qt::WhatsThisCursor};
+ if (c == "move")
+ return {Qt::SizeAllCursor};
+ if (c == "n-resize")
+ return {Qt::SizeVerCursor}; // ???
+ if (c == "ne-resize")
+ return {Qt::SizeBDiagCursor}; // ???
+ if (c == "nesw-resize")
+ return {Qt::SizeBDiagCursor};
+ if (c == "ns-resize")
+ return {Qt::SizeVerCursor};
+ if (c == "nw-resize")
+ return {Qt::SizeFDiagCursor}; // ???
+ if (c == "nwse-resize")
+ return {Qt::SizeFDiagCursor};
+ if (c == "no-drop")
+ return {Qt::ForbiddenCursor};
+ if (c == "none")
+ return {Qt::BlankCursor};
+ if (c == "not-allowed")
+ return {Qt::ForbiddenCursor};
+ if (c == "pointer")
+ return {Qt::PointingHandCursor};
+ if (c == "progress")
+ return {Qt::BusyCursor};
+ if (c == "row-resize")
+ return {Qt::SplitVCursor};
+ if (c == "s-resize")
+ return {Qt::SizeVerCursor}; // ???
+ if (c == "se-resize")
+ return {Qt::SizeFDiagCursor}; // ???
+ if (c == "sw-resize")
+ return {Qt::SizeBDiagCursor}; // ???
+ if (c == "text")
+ return {Qt::IBeamCursor};
+ if (c == "url")
+ return {Qt::ArrowCursor}; // ???
+ if (c == "w-resize")
+ return {Qt::SizeHorCursor}; // ???
+ if (c == "wait")
+ return {Qt::BusyCursor};
+ if (c == "zoom-in")
+ return {Qt::ArrowCursor}; // ???
+ qWarning(log) << QString("unknown cursor property \"%1\"").arg(c).toUtf8().constData();
+ return {Qt::ArrowCursor};
+}
+
+bool Selection::isValid() const
+{
+ return !selection.isEmpty();
+}
+
+void Selection::update()
+{
+ const auto addElement = [this](const Selection::Element &element,
+ const Selection::Element &end = {}) {
+ litehtml::tstring elemText;
+ element.element->get_text(elemText);
+ const QString textStr = QString::fromStdString(elemText);
+ if (!textStr.isEmpty()) {
+ QRect rect = toQRect(element.element->get_placement()).adjusted(-1, -1, 1, 1);
+ if (element.index < 0) { // fully selected
+ text += textStr;
+ } else if (end.element) { // select from element "to end"
+ if (element.element == end.element) {
+ // end.index is guaranteed to be >= element.index by caller, same for x
+ text += textStr.mid(element.index, end.index - element.index);
+ const int left = rect.left();
+ rect.setLeft(left + element.x);
+ rect.setRight(left + end.x);
+ } else {
+ text += textStr.mid(element.index);
+ rect.setLeft(rect.left() + element.x);
+ }
+ } else { // select from start of element
+ text += textStr.left(element.index);
+ rect.setRight(rect.left() + element.x);
+ }
+ selection.append(rect);
+ }
+ };
+
+ if (startElem.element && endElem.element) {
+ // Edge cases:
+ // start and end elements could be reversed or children of each other
+ Selection::Element start;
+ Selection::Element end;
+ std::tie(start, end) = getStartAndEnd(startElem, endElem);
+
+ selection.clear();
+ text.clear();
+
+ // Treats start element as a leaf even if it isn't, because it already contains all its
+ // children
+ addElement(start, end);
+ if (start.element != end.element) {
+ litehtml::element::ptr current = start.element;
+ do {
+ current = nextLeaf(current, end.element);
+ if (current == end.element)
+ addElement(end);
+ else
+ addElement({current, -1, -1});
+ } while (current != end.element);
+ }
+ } else {
+ selection = {};
+ text.clear();
+ }
+}
+
+QRect Selection::boundingRect() const
+{
+ QRect rect;
+ for (const QRect &r : selection)
+ rect = rect.united(r);
+ return rect;
+}
+
+DocumentContainer::DocumentContainer() = default;
+
+DocumentContainer::~DocumentContainer() = default;
+
+litehtml::uint_ptr DocumentContainer::create_font(const litehtml::tchar_t *faceName,
+ int size,
+ int weight,
+ litehtml::font_style italic,
+ unsigned int decoration,
+ litehtml::font_metrics *fm)
+{
+ const QStringList splitNames = QString::fromUtf8(faceName).split(',', QString::SkipEmptyParts);
+ QStringList familyNames;
+ std::transform(splitNames.cbegin(),
+ splitNames.cend(),
+ std::back_inserter(familyNames),
+ [this](const QString &s) {
+ // clean whitespace and quotes
+ QString name = s.trimmed();
+ if (name.startsWith('\"'))
+ name = name.mid(1);
+ if (name.endsWith('\"'))
+ name.chop(1);
+ const QString lowerName = name.toLower();
+ if (lowerName == "serif")
+ return serifFont();
+ if (lowerName == "sans-serif")
+ return sansSerifFont();
+ if (lowerName == "monospace")
+ return monospaceFont();
+ return name;
+ });
+ auto font = new QFont();
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
+ font->setFamilies(familyNames);
+#else
+ static const auto knownFamilies = QFontDatabase().families().toSet();
+ font->setFamily(familyNames.last());
+ for (const QString &name : qAsConst(familyNames))
+ if (knownFamilies.contains(name))
+ font->setFamily(name);
+#endif
+ font->setPixelSize(size);
+ font->setWeight(cssWeightToQtWeight(weight));
+ font->setStyle(toQFontStyle(italic));
+ // TODO: decoration
+ Q_UNUSED(decoration)
+ if (fm) {
+ const QFontMetrics metrics(*font);
+ fm->height = metrics.height();
+ fm->ascent = metrics.ascent();
+ fm->descent = metrics.descent();
+ fm->x_height = metrics.xHeight();
+ fm->draw_spaces = true;
+ }
+ return reinterpret_cast<litehtml::uint_ptr>(font);
+}
+
+void DocumentContainer::delete_font(litehtml::uint_ptr hFont)
+{
+ auto font = reinterpret_cast<Font *>(hFont);
+ delete font;
+}
+
+int DocumentContainer::text_width(const litehtml::tchar_t *text, litehtml::uint_ptr hFont)
+{
+ const QFontMetrics fm(toQFont(hFont));
+ return fm.horizontalAdvance(QString::fromUtf8(text));
+}
+
+void DocumentContainer::draw_text(litehtml::uint_ptr hdc,
+ const litehtml::tchar_t *text,
+ litehtml::uint_ptr hFont,
+ litehtml::web_color color,
+ const litehtml::position &pos)
+{
+ auto painter = toQPainter(hdc);
+ painter->setFont(toQFont(hFont));
+ painter->setPen(toQColor(color));
+ painter->drawText(toQRect(pos), 0, QString::fromUtf8(text));
+}
+
+int DocumentContainer::pt_to_px(int pt)
+{
+ return pt;
+}
+
+int DocumentContainer::get_default_font_size() const
+{
+ return m_defaultFont.pointSize();
+}
+
+const litehtml::tchar_t *DocumentContainer::get_default_font_name() const
+{
+ return m_defaultFontFamilyName.constData();
+}
+
+void DocumentContainer::draw_list_marker(litehtml::uint_ptr hdc, const litehtml::list_marker &marker)
+{
+ auto painter = toQPainter(hdc);
+ if (marker.image.empty()) {
+ if (marker.marker_type == litehtml::list_style_type_square) {
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(marker.color));
+ painter->drawRect(toQRect(marker.pos));
+ } else if (marker.marker_type == litehtml::list_style_type_disc) {
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(marker.color));
+ painter->drawEllipse(toQRect(marker.pos));
+ } else if (marker.marker_type == litehtml::list_style_type_circle) {
+ painter->setPen(toQColor(marker.color));
+ painter->setBrush(Qt::NoBrush);
+ painter->drawEllipse(toQRect(marker.pos));
+ } else {
+ // TODO we do not get information about index and font for e.g. decimal / roman
+ // at least draw a bullet
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(marker.color));
+ painter->drawEllipse(toQRect(marker.pos));
+ qWarning(log) << "list marker of type" << marker.marker_type << "not supported";
+ }
+ } else {
+ const QPixmap pixmap = getPixmap(QString::fromStdString(marker.image),
+ QString::fromStdString(marker.baseurl));
+ painter->drawPixmap(toQRect(marker.pos), pixmap);
+ }
+}
+
+void DocumentContainer::load_image(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ bool redraw_on_ready)
+{
+ const auto qtSrc = QString::fromUtf8(src);
+ const auto qtBaseUrl = QString::fromUtf8(baseurl);
+ Q_UNUSED(redraw_on_ready)
+ qDebug(log) << "load_image:" << QString("src = \"%1\";").arg(qtSrc).toUtf8().constData()
+ << QString("base = \"%1\"").arg(qtBaseUrl).toUtf8().constData();
+ const QUrl url = resolveUrl(qtSrc, qtBaseUrl);
+ if (m_pixmaps.contains(url))
+ return;
+
+ QPixmap pixmap;
+ pixmap.loadFromData(m_dataCallback(url));
+ m_pixmaps.insert(url, pixmap);
+}
+
+void DocumentContainer::get_image_size(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ litehtml::size &sz)
+{
+ const auto qtSrc = QString::fromUtf8(src);
+ const auto qtBaseUrl = QString::fromUtf8(baseurl);
+ if (qtSrc.isEmpty()) // for some reason that happens
+ return;
+ qDebug(log) << "get_image_size:" << QString("src = \"%1\";").arg(qtSrc).toUtf8().constData()
+ << QString("base = \"%1\"").arg(qtBaseUrl).toUtf8().constData();
+ const QPixmap pm = getPixmap(qtSrc, qtBaseUrl);
+ sz.width = pm.width();
+ sz.height = pm.height();
+}
+
+void DocumentContainer::drawSelection(QPainter *painter, const QRect &clip) const
+{
+ painter->save();
+ painter->setClipRect(clip, Qt::IntersectClip);
+ for (const QRect &r : m_selection.selection) {
+ const QRect clientRect = r.translated(-m_scrollPosition);
+ const QPalette palette = m_paletteCallback();
+ painter->fillRect(clientRect, palette.brush(QPalette::Highlight));
+ }
+ painter->restore();
+}
+
+void DocumentContainer::draw_background(litehtml::uint_ptr hdc, const litehtml::background_paint &bg)
+{
+ // TODO
+ auto painter = toQPainter(hdc);
+ if (bg.is_root) {
+ // TODO ?
+ drawSelection(painter, toQRect(bg.border_box));
+ return;
+ }
+ painter->save();
+ painter->setClipRect(toQRect(bg.clip_box));
+ const QRegion horizontalMiddle(
+ QRect(bg.border_box.x,
+ bg.border_box.y + bg.border_radius.top_left_y,
+ bg.border_box.width,
+ bg.border_box.height - bg.border_radius.top_left_y - bg.border_radius.bottom_left_y));
+ const QRegion horizontalTop(
+ QRect(bg.border_box.x + bg.border_radius.top_left_x,
+ bg.border_box.y,
+ bg.border_box.width - bg.border_radius.top_left_x - bg.border_radius.top_right_x,
+ bg.border_radius.top_left_y));
+ const QRegion horizontalBottom(QRect(bg.border_box.x + bg.border_radius.bottom_left_x,
+ bg.border_box.bottom() - bg.border_radius.bottom_left_y,
+ bg.border_box.width - bg.border_radius.bottom_left_x
+ - bg.border_radius.bottom_right_x,
+ bg.border_radius.bottom_left_y));
+ const QRegion topLeft(QRect(bg.border_box.left(),
+ bg.border_box.top(),
+ 2 * bg.border_radius.top_left_x,
+ 2 * bg.border_radius.top_left_y),
+ QRegion::Ellipse);
+ const QRegion topRight(QRect(bg.border_box.right() - 2 * bg.border_radius.top_right_x,
+ bg.border_box.top(),
+ 2 * bg.border_radius.top_right_x,
+ 2 * bg.border_radius.top_right_y),
+ QRegion::Ellipse);
+ const QRegion bottomLeft(QRect(bg.border_box.left(),
+ bg.border_box.bottom() - 2 * bg.border_radius.bottom_left_y,
+ 2 * bg.border_radius.bottom_left_x,
+ 2 * bg.border_radius.bottom_left_y),
+ QRegion::Ellipse);
+ const QRegion bottomRight(QRect(bg.border_box.right() - 2 * bg.border_radius.bottom_right_x,
+ bg.border_box.bottom() - 2 * bg.border_radius.bottom_right_y,
+ 2 * bg.border_radius.bottom_right_x,
+ 2 * bg.border_radius.bottom_right_y),
+ QRegion::Ellipse);
+ const QRegion clipRegion = horizontalMiddle.united(horizontalTop)
+ .united(horizontalBottom)
+ .united(topLeft)
+ .united(topRight)
+ .united(bottomLeft)
+ .united(bottomRight);
+ painter->setClipRegion(clipRegion, Qt::IntersectClip);
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(toQColor(bg.color));
+ painter->drawRect(bg.border_box.x, bg.border_box.y, bg.border_box.width, bg.border_box.height);
+ drawSelection(painter, toQRect(bg.border_box));
+ if (!bg.image.empty()) {
+ const QPixmap pixmap = getPixmap(QString::fromStdString(bg.image),
+ QString::fromStdString(bg.baseurl));
+ if (bg.repeat == litehtml::background_repeat_no_repeat) {
+ painter->drawPixmap(QRect(bg.position_x,
+ bg.position_y,
+ bg.image_size.width,
+ bg.image_size.height),
+ pixmap);
+ } else if (bg.repeat == litehtml::background_repeat_repeat_x) {
+ if (bg.image_size.width > 0) {
+ int x = bg.border_box.left();
+ while (x <= bg.border_box.right()) {
+ painter->drawPixmap(QRect(x,
+ bg.border_box.top(),
+ bg.image_size.width,
+ bg.image_size.height),
+ pixmap);
+ x += bg.image_size.width;
+ }
+ }
+ } else {
+ qWarning(log) << "unsupported background repeat" << bg.repeat;
+ }
+ }
+ painter->restore();
+}
+
+void DocumentContainer::draw_borders(litehtml::uint_ptr hdc,
+ const litehtml::borders &borders,
+ const litehtml::position &draw_pos,
+ bool root)
+{
+ Q_UNUSED(root)
+ // TODO: special border styles
+ auto painter = toQPainter(hdc);
+ if (borders.top.style != litehtml::border_style_none
+ && borders.top.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.top));
+ painter->drawLine(draw_pos.left() + borders.radius.top_left_x,
+ draw_pos.top(),
+ draw_pos.right() - borders.radius.top_right_x,
+ draw_pos.top());
+ painter->drawArc(draw_pos.left(),
+ draw_pos.top(),
+ 2 * borders.radius.top_left_x,
+ 2 * borders.radius.top_left_y,
+ 90 * 16,
+ 90 * 16);
+ painter->drawArc(draw_pos.right() - 2 * borders.radius.top_right_x,
+ draw_pos.top(),
+ 2 * borders.radius.top_right_x,
+ 2 * borders.radius.top_right_y,
+ 0,
+ 90 * 16);
+ }
+ if (borders.bottom.style != litehtml::border_style_none
+ && borders.bottom.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.bottom));
+ painter->drawLine(draw_pos.left() + borders.radius.bottom_left_x,
+ draw_pos.bottom(),
+ draw_pos.right() - borders.radius.bottom_right_x,
+ draw_pos.bottom());
+ painter->drawArc(draw_pos.left(),
+ draw_pos.bottom() - 2 * borders.radius.bottom_left_y,
+ 2 * borders.radius.bottom_left_x,
+ 2 * borders.radius.bottom_left_y,
+ 180 * 16,
+ 90 * 16);
+ painter->drawArc(draw_pos.right() - 2 * borders.radius.bottom_right_x,
+ draw_pos.bottom() - 2 * borders.radius.bottom_right_y,
+ 2 * borders.radius.bottom_right_x,
+ 2 * borders.radius.bottom_right_y,
+ 270 * 16,
+ 90 * 16);
+ }
+ if (borders.left.style != litehtml::border_style_none
+ && borders.left.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.left));
+ painter->drawLine(draw_pos.left(),
+ draw_pos.top() + borders.radius.top_left_y,
+ draw_pos.left(),
+ draw_pos.bottom() - borders.radius.bottom_left_y);
+ }
+ if (borders.right.style != litehtml::border_style_none
+ && borders.right.style != litehtml::border_style_hidden) {
+ painter->setPen(borderPen(borders.right));
+ painter->drawLine(draw_pos.right(),
+ draw_pos.top() + borders.radius.top_right_y,
+ draw_pos.right(),
+ draw_pos.bottom() - borders.radius.bottom_right_y);
+ }
+}
+
+void DocumentContainer::set_caption(const litehtml::tchar_t *caption)
+{
+ m_caption = QString::fromUtf8(caption);
+}
+
+void DocumentContainer::set_base_url(const litehtml::tchar_t *base_url)
+{
+ m_baseUrl = QString::fromUtf8(base_url);
+}
+
+void DocumentContainer::link(const std::shared_ptr<litehtml::document> &doc,
+ const litehtml::element::ptr &el)
+{
+ // TODO
+ qDebug(log) << "link";
+ Q_UNUSED(doc)
+ Q_UNUSED(el)
+}
+
+void DocumentContainer::on_anchor_click(const litehtml::tchar_t *url,
+ const litehtml::element::ptr &el)
+{
+ Q_UNUSED(el)
+ if (!m_blockLinks)
+ m_linkCallback(resolveUrl(QString::fromUtf8(url), m_baseUrl));
+}
+
+void DocumentContainer::set_cursor(const litehtml::tchar_t *cursor)
+{
+ m_cursorCallback(toQCursor(QString::fromUtf8(cursor)));
+}
+
+void DocumentContainer::transform_text(litehtml::tstring &text, litehtml::text_transform tt)
+{
+ // TODO
+ qDebug(log) << "transform_text";
+ Q_UNUSED(text)
+ Q_UNUSED(tt)
+}
+
+void DocumentContainer::import_css(litehtml::tstring &text,
+ const litehtml::tstring &url,
+ litehtml::tstring &baseurl)
+{
+ const QUrl actualUrl = resolveUrl(QString::fromStdString(url), QString::fromStdString(baseurl));
+ const QString urlString = actualUrl.toString(QUrl::None);
+ const int lastSlash = urlString.lastIndexOf('/');
+ baseurl = urlString.left(lastSlash).toStdString();
+ text = QString::fromUtf8(m_dataCallback(actualUrl)).toStdString();
+}
+
+void DocumentContainer::set_clip(const litehtml::position &pos,
+ const litehtml::border_radiuses &bdr_radius,
+ bool valid_x,
+ bool valid_y)
+{
+ // TODO
+ qDebug(log) << "set_clip";
+ Q_UNUSED(pos)
+ Q_UNUSED(bdr_radius)
+ Q_UNUSED(valid_x)
+ Q_UNUSED(valid_y)
+}
+
+void DocumentContainer::del_clip()
+{
+ // TODO
+ qDebug(log) << "del_clip";
+}
+
+void DocumentContainer::get_client_rect(litehtml::position &client) const
+{
+ client = {m_clientRect.x(), m_clientRect.y(), m_clientRect.width(), m_clientRect.height()};
+}
+
+std::shared_ptr<litehtml::element> DocumentContainer::create_element(
+ const litehtml::tchar_t *tag_name,
+ const litehtml::string_map &attributes,
+ const std::shared_ptr<litehtml::document> &doc)
+{
+ // TODO
+ qDebug(log) << "create_element" << QString::fromUtf8(tag_name);
+ Q_UNUSED(attributes)
+ Q_UNUSED(doc)
+ return {};
+}
+
+void DocumentContainer::get_media_features(litehtml::media_features &media) const
+{
+ media.type = litehtml::media_type_screen;
+ // TODO
+ qDebug(log) << "get_media_features";
+}
+
+void DocumentContainer::get_language(litehtml::tstring &language, litehtml::tstring &culture) const
+{
+ // TODO
+ qDebug(log) << "get_language";
+ Q_UNUSED(language)
+ Q_UNUSED(culture)
+}
+
+void DocumentContainer::setScrollPosition(const QPoint &pos)
+{
+ m_scrollPosition = pos;
+}
+
+void DocumentContainer::setDocument(litehtml::document::ptr document)
+{
+ m_document = document;
+ m_pixmaps.clear();
+ m_selection = {};
+}
+
+litehtml::document::ptr DocumentContainer::document() const
+{
+ return m_document;
+}
+
+void DocumentContainer::render(int width, int height)
+{
+ m_clientRect = {0, 0, width, height};
+ if (!m_document)
+ return;
+ m_document->render(width);
+ m_selection.update();
+}
+
+QVector<QRect> DocumentContainer::mousePressEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button)
+{
+ if (!m_document || button != Qt::LeftButton)
+ return {};
+ QVector<QRect> redrawRects;
+ // selection
+ if (m_selection.isValid())
+ redrawRects.append(m_selection.boundingRect());
+ m_selection = {};
+ m_selection.selectionStartDocumentPos = documentPos;
+ m_selection.startElem = deepest_child_at_point(m_document,
+ documentPos,
+ viewportPos,
+ m_selection.mode);
+ // post to litehtml
+ litehtml::position::vector redrawBoxes;
+ if (m_document->on_lbutton_down(documentPos.x(),
+ documentPos.y(),
+ viewportPos.x(),
+ viewportPos.y(),
+ redrawBoxes)) {
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ }
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::mouseMoveEvent(const QPoint &documentPos,
+ const QPoint &viewportPos)
+{
+ if (!m_document)
+ return {};
+ QVector<QRect> redrawRects;
+ // selection
+ if (m_selection.isSelecting
+ || (!m_selection.selectionStartDocumentPos.isNull()
+ && (m_selection.selectionStartDocumentPos - documentPos).manhattanLength()
+ >= kDragDistance
+ && m_selection.startElem.element)) {
+ const Selection::Element element = deepest_child_at_point(m_document,
+ documentPos,
+ viewportPos,
+ m_selection.mode);
+ if (element.element) {
+ m_selection.endElem = element;
+ redrawRects.append(m_selection.boundingRect()); // redraw old selection area
+ m_selection.update();
+ redrawRects.append(m_selection.boundingRect());
+ }
+ m_selection.isSelecting = true;
+ }
+ litehtml::position::vector redrawBoxes;
+ if (m_document->on_mouse_over(documentPos.x(),
+ documentPos.y(),
+ viewportPos.x(),
+ viewportPos.y(),
+ redrawBoxes)) {
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ }
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::mouseReleaseEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button)
+{
+ if (!m_document || button != Qt::LeftButton)
+ return {};
+ QVector<QRect> redrawRects;
+ // selection
+ m_selection.isSelecting = false;
+ m_selection.selectionStartDocumentPos = {};
+ if (m_selection.isValid())
+ m_blockLinks = true;
+ else
+ m_selection = {};
+ litehtml::position::vector redrawBoxes;
+ if (m_document->on_lbutton_up(documentPos.x(),
+ documentPos.y(),
+ viewportPos.x(),
+ viewportPos.y(),
+ redrawBoxes)) {
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ }
+ m_blockLinks = false;
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::mouseDoubleClickEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button)
+{
+ if (!m_document || button != Qt::LeftButton)
+ return {};
+ QVector<QRect> redrawRects;
+ m_selection = {};
+ m_selection.mode = Selection::Mode::Word;
+ const Selection::Element element = deepest_child_at_point(m_document,
+ documentPos,
+ viewportPos,
+ m_selection.mode);
+ if (element.element) {
+ m_selection.startElem = element;
+ m_selection.endElem = m_selection.startElem;
+ m_selection.isSelecting = true;
+ m_selection.update();
+ if (m_selection.isValid())
+ redrawRects.append(m_selection.boundingRect());
+ } else {
+ if (m_selection.isValid())
+ redrawRects.append(m_selection.boundingRect());
+ m_selection = {};
+ }
+ return redrawRects;
+}
+
+QVector<QRect> DocumentContainer::leaveEvent()
+{
+ if (!m_document)
+ return {};
+ litehtml::position::vector redrawBoxes;
+ if (m_document->on_mouse_leave(redrawBoxes)) {
+ QVector<QRect> redrawRects;
+ for (const litehtml::position &box : redrawBoxes)
+ redrawRects.append(toQRect(box));
+ return redrawRects;
+ }
+ return {};
+}
+
+QString DocumentContainer::caption() const
+{
+ return m_caption;
+}
+
+QString DocumentContainer::selectedText() const
+{
+ return m_selection.text;
+}
+
+void DocumentContainer::setDefaultFont(const QFont &font)
+{
+ m_defaultFont = font;
+ m_defaultFontFamilyName = m_defaultFont.family().toUtf8();
+}
+
+QFont DocumentContainer::defaultFont() const
+{
+ return m_defaultFont;
+}
+
+void DocumentContainer::setDataCallback(const DocumentContainer::DataCallback &callback)
+{
+ m_dataCallback = callback;
+}
+
+void DocumentContainer::setCursorCallback(const DocumentContainer::CursorCallback &callback)
+{
+ m_cursorCallback = callback;
+}
+
+void DocumentContainer::setLinkCallback(const DocumentContainer::LinkCallback &callback)
+{
+ m_linkCallback = callback;
+}
+
+void DocumentContainer::setPaletteCallback(const DocumentContainer::PaletteCallback &callback)
+{
+ m_paletteCallback = callback;
+}
+
+QPixmap DocumentContainer::getPixmap(const QString &imageUrl, const QString &baseUrl)
+{
+ const QString actualBaseurl = baseUrl.isEmpty() ? m_baseUrl : baseUrl;
+ const QUrl url = resolveUrl(imageUrl, baseUrl);
+ if (!m_pixmaps.contains(url)) {
+ qWarning(log) << "draw_background: pixmap not loaded for" << url;
+ return {};
+ }
+ return m_pixmaps.value(url);
+}
+
+QString DocumentContainer::serifFont() const
+{
+ // TODO make configurable
+ return "Times New Roman";
+}
+
+QString DocumentContainer::sansSerifFont() const
+{
+ // TODO make configurable
+ return "Arial";
+}
+
+QString DocumentContainer::monospaceFont() const
+{
+ // TODO make configurable
+ return "Courier";
+}
+
+QUrl DocumentContainer::resolveUrl(const QString &url, const QString &baseUrl) const
+{
+ const QUrl qurl(url);
+ if (qurl.isRelative() && !qurl.path(QUrl::FullyEncoded).isEmpty()) {
+ const QString actualBaseurl = baseUrl.isEmpty() ? m_baseUrl : baseUrl;
+ QUrl resolvedUrl(actualBaseurl + '/' + url);
+ resolvedUrl.setPath(QDir::cleanPath(resolvedUrl.path(QUrl::FullyEncoded)));
+ return resolvedUrl;
+ }
+ return qurl;
+}
diff --git a/src/plugins/help/qlitehtml/container_qpainter.h b/src/plugins/help/qlitehtml/container_qpainter.h
new file mode 100644
index 0000000000..5d556e2c91
--- /dev/null
+++ b/src/plugins/help/qlitehtml/container_qpainter.h
@@ -0,0 +1,181 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <litehtml.h>
+
+#include <QFont>
+#include <QHash>
+#include <QPixmap>
+#include <QRect>
+#include <QString>
+#include <QUrl>
+
+#include <functional>
+
+class Selection
+{
+public:
+ struct Element
+ {
+ litehtml::element::ptr element;
+ int index = -1;
+ int x = -1;
+ };
+
+ enum class Mode { Free, Word };
+
+ bool isValid() const;
+
+ void update();
+ QRect boundingRect() const;
+
+ Element startElem;
+ Element endElem;
+ QVector<QRect> selection;
+ QString text;
+
+ QPoint selectionStartDocumentPos;
+ Mode mode = Mode::Free;
+ bool isSelecting = false;
+};
+
+class DocumentContainer : public litehtml::document_container
+{
+public:
+ DocumentContainer();
+ virtual ~DocumentContainer();
+
+ litehtml::uint_ptr create_font(const litehtml::tchar_t *faceName,
+ int size,
+ int weight,
+ litehtml::font_style italic,
+ unsigned int decoration,
+ litehtml::font_metrics *fm) override;
+ void delete_font(litehtml::uint_ptr hFont) override;
+ int text_width(const litehtml::tchar_t *text, litehtml::uint_ptr hFont) override;
+ void draw_text(litehtml::uint_ptr hdc,
+ const litehtml::tchar_t *text,
+ litehtml::uint_ptr hFont,
+ litehtml::web_color color,
+ const litehtml::position &pos) override;
+ int pt_to_px(int pt) override;
+ int get_default_font_size() const override;
+ const litehtml::tchar_t *get_default_font_name() const override;
+ void draw_list_marker(litehtml::uint_ptr hdc, const litehtml::list_marker &marker) override;
+ void load_image(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ bool redraw_on_ready) override;
+ void get_image_size(const litehtml::tchar_t *src,
+ const litehtml::tchar_t *baseurl,
+ litehtml::size &sz) override;
+ void draw_background(litehtml::uint_ptr hdc, const litehtml::background_paint &bg) override;
+ void draw_borders(litehtml::uint_ptr hdc,
+ const litehtml::borders &borders,
+ const litehtml::position &draw_pos,
+ bool root) override;
+ void set_caption(const litehtml::tchar_t *caption) override;
+ void set_base_url(const litehtml::tchar_t *base_url) override;
+ void link(const std::shared_ptr<litehtml::document> &doc,
+ const litehtml::element::ptr &el) override;
+ void on_anchor_click(const litehtml::tchar_t *url, const litehtml::element::ptr &el) override;
+ void set_cursor(const litehtml::tchar_t *cursor) override;
+ void transform_text(litehtml::tstring &text, litehtml::text_transform tt) override;
+ void import_css(litehtml::tstring &text,
+ const litehtml::tstring &url,
+ litehtml::tstring &baseurl) override;
+ void set_clip(const litehtml::position &pos,
+ const litehtml::border_radiuses &bdr_radius,
+ bool valid_x,
+ bool valid_y) override;
+ void del_clip() override;
+ void get_client_rect(litehtml::position &client) const override;
+ std::shared_ptr<litehtml::element> create_element(
+ const litehtml::tchar_t *tag_name,
+ const litehtml::string_map &attributes,
+ const std::shared_ptr<litehtml::document> &doc) override;
+ void get_media_features(litehtml::media_features &media) const override;
+ void get_language(litehtml::tstring &language, litehtml::tstring &culture) const override;
+
+ void setScrollPosition(const QPoint &pos);
+ void setDocument(litehtml::document::ptr document);
+ litehtml::document::ptr document() const;
+ void render(int width, int height);
+
+ // these return areas to redraw in document space
+ QVector<QRect> mousePressEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button);
+ QVector<QRect> mouseMoveEvent(const QPoint &documentPos, const QPoint &viewportPos);
+ QVector<QRect> mouseReleaseEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button);
+ QVector<QRect> mouseDoubleClickEvent(const QPoint &documentPos,
+ const QPoint &viewportPos,
+ Qt::MouseButton button);
+ QVector<QRect> leaveEvent();
+
+ QString caption() const;
+ QString selectedText() const;
+
+ void setDefaultFont(const QFont &font);
+ QFont defaultFont() const;
+
+ using DataCallback = std::function<QByteArray(QUrl)>;
+ void setDataCallback(const DataCallback &callback);
+
+ using CursorCallback = std::function<void(QCursor)>;
+ void setCursorCallback(const CursorCallback &callback);
+
+ using LinkCallback = std::function<void(QUrl)>;
+ void setLinkCallback(const LinkCallback &callback);
+
+ using PaletteCallback = std::function<QPalette()>;
+ void setPaletteCallback(const PaletteCallback &callback);
+
+private:
+ QPixmap getPixmap(const QString &imageUrl, const QString &baseUrl);
+ QString serifFont() const;
+ QString sansSerifFont() const;
+ QString monospaceFont() const;
+ QUrl resolveUrl(const QString &url, const QString &baseUrl) const;
+ void drawSelection(QPainter *painter, const QRect &clip) const;
+
+ litehtml::document::ptr m_document;
+ QString m_baseUrl;
+ QRect m_clientRect;
+ QPoint m_scrollPosition;
+ QString m_caption;
+ QFont m_defaultFont = QFont(sansSerifFont(), 16);
+ QByteArray m_defaultFontFamilyName = m_defaultFont.family().toUtf8();
+ QHash<QUrl, QPixmap> m_pixmaps;
+ Selection m_selection;
+ DataCallback m_dataCallback;
+ CursorCallback m_cursorCallback;
+ LinkCallback m_linkCallback;
+ PaletteCallback m_paletteCallback;
+ bool m_blockLinks = false;
+};
diff --git a/src/plugins/help/qlitehtml/qlitehtml.pri b/src/plugins/help/qlitehtml/qlitehtml.pri
new file mode 100644
index 0000000000..4520512b10
--- /dev/null
+++ b/src/plugins/help/qlitehtml/qlitehtml.pri
@@ -0,0 +1,13 @@
+HEADERS += \
+ $$PWD/container_qpainter.h \
+ $$PWD/qlitehtmlwidget.h
+
+SOURCES += \
+ $$PWD/container_qpainter.cpp \
+ $$PWD/qlitehtmlwidget.cpp
+
+INCLUDEPATH += $$PWD $$LITEHTML_INSTALL_DIR/include $$LITEHTML_INSTALL_DIR/include/litehtml
+LIBS += -L$$LITEHTML_INSTALL_DIR/lib -llitehtml -lgumbo
+
+win32: PRE_TARGET_DEPS += $$LITEHTML_INSTALL_DIR/lib/litehtml.lib $$LITEHTML_INSTALL_DIR/lib/gumbo.lib
+else:unix: PRE_TARGET_DEPS += $$LITEHTML_INSTALL_DIR/lib/liblitehtml.a $$LITEHTML_INSTALL_DIR/lib/libgumbo.a
diff --git a/src/plugins/help/qlitehtml/qlitehtmlwidget.cpp b/src/plugins/help/qlitehtml/qlitehtmlwidget.cpp
new file mode 100644
index 0000000000..db29cd0ecb
--- /dev/null
+++ b/src/plugins/help/qlitehtml/qlitehtmlwidget.cpp
@@ -0,0 +1,574 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "qlitehtmlwidget.h"
+
+#include "container_qpainter.h"
+
+#include <QDebug>
+#include <QPaintEvent>
+#include <QPainter>
+#include <QScrollBar>
+#include <QStyle>
+#include <QTimer>
+
+#include <litehtml.h>
+
+const int kScrollBarStep = 40;
+
+// TODO copied from litehtml/include/master.css
+const char mastercss[] = R"RAW(
+html {
+ display: block;
+height:100%;
+width:100%;
+position: relative;
+}
+
+head {
+ display: none
+}
+
+meta {
+ display: none
+}
+
+title {
+ display: none
+}
+
+link {
+ display: none
+}
+
+style {
+ display: none
+}
+
+script {
+ display: none
+}
+
+body {
+display:block;
+ margin:8px;
+ height:100%;
+width:100%;
+}
+
+p {
+display:block;
+ margin-top:1em;
+ margin-bottom:1em;
+}
+
+b, strong {
+display:inline;
+ font-weight:bold;
+}
+
+i, em {
+display:inline;
+ font-style:italic;
+}
+
+center
+{
+ text-align:center;
+display:block;
+}
+
+a:link
+{
+ text-decoration: underline;
+color: #00f;
+cursor: pointer;
+}
+
+h1, h2, h3, h4, h5, h6, div {
+display:block;
+}
+
+h1 {
+ font-weight:bold;
+ margin-top:0.67em;
+ margin-bottom:0.67em;
+ font-size: 2em;
+}
+
+h2 {
+ font-weight:bold;
+ margin-top:0.83em;
+ margin-bottom:0.83em;
+ font-size: 1.5em;
+}
+
+h3 {
+ font-weight:bold;
+ margin-top:1em;
+ margin-bottom:1em;
+ font-size:1.17em;
+}
+
+h4 {
+ font-weight:bold;
+ margin-top:1.33em;
+ margin-bottom:1.33em
+}
+
+h5 {
+ font-weight:bold;
+ margin-top:1.67em;
+ margin-bottom:1.67em;
+ font-size:.83em;
+}
+
+h6 {
+ font-weight:bold;
+ margin-top:2.33em;
+ margin-bottom:2.33em;
+ font-size:.67em;
+}
+
+br {
+display:inline-block;
+}
+
+br[clear="all"]
+{
+clear:both;
+}
+
+br[clear="left"]
+{
+clear:left;
+}
+
+br[clear="right"]
+{
+clear:right;
+}
+
+span {
+ display:inline
+}
+
+img {
+display: inline-block;
+}
+
+img[align="right"]
+{
+ float: right;
+}
+
+img[align="left"]
+{
+ float: left;
+}
+
+hr {
+display: block;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ margin-left: auto;
+ margin-right: auto;
+ border-style: inset;
+ border-width: 1px
+}
+
+
+/***************** TABLES ********************/
+
+table {
+display: table;
+ border-collapse: separate;
+ border-spacing: 2px;
+ border-top-color:gray;
+ border-left-color:gray;
+ border-bottom-color:black;
+ border-right-color:black;
+}
+
+tbody, tfoot, thead {
+display:table-row-group;
+ vertical-align:middle;
+}
+
+tr {
+display: table-row;
+ vertical-align: inherit;
+ border-color: inherit;
+}
+
+td, th {
+display: table-cell;
+ vertical-align: inherit;
+ border-width:1px;
+padding:1px;
+}
+
+th {
+ font-weight: bold;
+}
+
+table[border] {
+ border-style:solid;
+}
+
+table[border|=0] {
+ border-style:none;
+}
+
+table[border] td, table[border] th {
+ border-style:solid;
+ border-top-color:black;
+ border-left-color:black;
+ border-bottom-color:gray;
+ border-right-color:gray;
+}
+
+table[border|=0] td, table[border|=0] th {
+ border-style:none;
+}
+
+caption {
+display: table-caption;
+}
+
+td[nowrap], th[nowrap] {
+ white-space:nowrap;
+}
+
+tt, code, kbd, samp {
+ font-family: monospace
+}
+pre, xmp, plaintext, listing {
+display: block;
+ font-family: monospace;
+ white-space: pre;
+margin: 1em 0
+}
+
+/***************** LISTS ********************/
+
+ul, menu, dir {
+display: block;
+ list-style-type: disc;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 40px
+}
+
+ol {
+display: block;
+ list-style-type: decimal;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 40px
+}
+
+li {
+display: list-item;
+}
+
+ul ul, ol ul {
+ list-style-type: circle;
+}
+
+ol ol ul, ol ul ul, ul ol ul, ul ul ul {
+ list-style-type: square;
+}
+
+dd {
+display: block;
+ margin-left: 40px;
+}
+
+dl {
+display: block;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+dt {
+display: block;
+}
+
+ol ul, ul ol, ul ul, ol ol {
+ margin-top: 0;
+ margin-bottom: 0
+}
+
+blockquote {
+display: block;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ margin-left: 40px;
+ margin-left: 40px;
+}
+
+/*********** FORM ELEMENTS ************/
+
+form {
+display: block;
+ margin-top: 0em;
+}
+
+option {
+display: none;
+}
+
+input, textarea, keygen, select, button, isindex {
+margin: 0em;
+color: initial;
+ line-height: normal;
+ text-transform: none;
+ text-indent: 0;
+ text-shadow: none;
+display: inline-block;
+}
+input[type="hidden"] {
+display: none;
+}
+
+
+article, aside, footer, header, hgroup, nav, section
+{
+display: block;
+}
+)RAW";
+
+class QLiteHtmlWidgetPrivate
+{
+public:
+ litehtml::context context;
+ QUrl url;
+ DocumentContainer documentContainer;
+};
+
+QLiteHtmlWidget::QLiteHtmlWidget(QWidget *parent)
+ : QAbstractScrollArea(parent)
+ , d(new QLiteHtmlWidgetPrivate)
+{
+ setMouseTracking(true);
+ horizontalScrollBar()->setSingleStep(kScrollBarStep);
+ verticalScrollBar()->setSingleStep(kScrollBarStep);
+
+ d->documentContainer.setCursorCallback([this](const QCursor &c) { viewport()->setCursor(c); });
+ d->documentContainer.setPaletteCallback([this] { return palette(); });
+ d->documentContainer.setLinkCallback([this](const QUrl &url) {
+ QUrl fullUrl = url;
+ if (url.isRelative() && url.path(QUrl::FullyEncoded).isEmpty()) { // fragment/anchor only
+ fullUrl = d->url;
+ fullUrl.setFragment(url.fragment(QUrl::FullyEncoded));
+ }
+ // delay because document may not be changed directly during this callback
+ QTimer::singleShot(0, this, [this, fullUrl] { emit linkClicked(fullUrl); });
+ });
+
+ // TODO adapt mastercss to palette (default text & background color)
+ d->context.load_master_stylesheet(mastercss);
+}
+
+QLiteHtmlWidget::~QLiteHtmlWidget()
+{
+ delete d;
+}
+
+void QLiteHtmlWidget::setUrl(const QUrl &url)
+{
+ d->url = url;
+ QUrl urlWithoutAnchor = url;
+ urlWithoutAnchor.setFragment({});
+ const QString urlString = urlWithoutAnchor.toString(QUrl::None);
+ const int lastSlash = urlString.lastIndexOf('/');
+ const QString baseUrl = lastSlash >= 0 ? urlString.left(lastSlash) : urlString;
+ d->documentContainer.set_base_url(baseUrl.toUtf8().constData());
+}
+
+QUrl QLiteHtmlWidget::url() const
+{
+ return d->url;
+}
+
+void QLiteHtmlWidget::setHtml(const QString &content)
+{
+ litehtml::document::ptr doc = litehtml::document::createFromUTF8(content.toUtf8().constData(),
+ &d->documentContainer,
+ &d->context);
+ d->documentContainer.setDocument(doc);
+ verticalScrollBar()->setValue(0);
+ horizontalScrollBar()->setValue(0);
+ render();
+}
+
+QString QLiteHtmlWidget::title() const
+{
+ return d->documentContainer.caption();
+}
+
+void QLiteHtmlWidget::setDefaultFont(const QFont &font)
+{
+ d->documentContainer.setDefaultFont(font);
+ render();
+}
+
+QFont QLiteHtmlWidget::defaultFont() const
+{
+ return d->documentContainer.defaultFont();
+}
+
+void QLiteHtmlWidget::scrollToAnchor(const QString &name)
+{
+ if (!d->documentContainer.document())
+ return;
+ horizontalScrollBar()->setValue(0);
+ if (name.isEmpty()) {
+ verticalScrollBar()->setValue(0);
+ return;
+ }
+ litehtml::element::ptr element = d->documentContainer.document()->root()->select_one(
+ QString("#%1").arg(name).toStdString());
+ if (!element) {
+ element = d->documentContainer.document()->root()->select_one(
+ QString("[name=%1]").arg(name).toStdString());
+ }
+ if (element) {
+ const int y = element->get_placement().y;
+ verticalScrollBar()->setValue(std::min(y, verticalScrollBar()->maximum()));
+ }
+}
+
+void QLiteHtmlWidget::setResourceHandler(const QLiteHtmlWidget::ResourceHandler &handler)
+{
+ d->documentContainer.setDataCallback(handler);
+}
+
+QString QLiteHtmlWidget::selectedText() const
+{
+ return d->documentContainer.selectedText();
+}
+
+void QLiteHtmlWidget::paintEvent(QPaintEvent *event)
+{
+ if (!d->documentContainer.document())
+ return;
+ d->documentContainer.setScrollPosition(scrollPosition());
+ const QPoint pos = -scrollPosition();
+ const QRect r = event->rect();
+ const litehtml::position clip = {r.x(), r.y(), r.width(), r.height()};
+ QPainter p(viewport());
+ d->documentContainer.document()->draw(reinterpret_cast<litehtml::uint_ptr>(&p),
+ pos.x(),
+ pos.y(),
+ &clip);
+}
+
+void QLiteHtmlWidget::resizeEvent(QResizeEvent *event)
+{
+ QAbstractScrollArea::resizeEvent(event);
+ render();
+}
+
+void QLiteHtmlWidget::mouseMoveEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r : d->documentContainer.mouseMoveEvent(pos, viewportPos))
+ viewport()->update(r.translated(-scrollPosition()));
+}
+
+void QLiteHtmlWidget::mousePressEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r : d->documentContainer.mousePressEvent(pos, viewportPos, event->button()))
+ viewport()->update(r.translated(-scrollPosition()));
+}
+
+void QLiteHtmlWidget::mouseReleaseEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r : d->documentContainer.mouseReleaseEvent(pos, viewportPos, event->button()))
+ viewport()->update(r.translated(-scrollPosition()));
+}
+
+void QLiteHtmlWidget::mouseDoubleClickEvent(QMouseEvent *event)
+{
+ QPoint viewportPos;
+ QPoint pos;
+ htmlPos(event->pos(), &viewportPos, &pos);
+ for (const QRect &r :
+ d->documentContainer.mouseDoubleClickEvent(pos, viewportPos, event->button())) {
+ viewport()->update(r.translated(-scrollPosition()));
+ }
+}
+
+void QLiteHtmlWidget::leaveEvent(QEvent *event)
+{
+ Q_UNUSED(event)
+ for (const QRect &r : d->documentContainer.leaveEvent())
+ viewport()->update(r.translated(-scrollPosition()));
+}
+
+void QLiteHtmlWidget::render()
+{
+ if (!d->documentContainer.document())
+ return;
+ const int scrollbarWidth = style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, this);
+ const int w = width() - scrollbarWidth - 2;
+ d->documentContainer.render(w, viewport()->height());
+ horizontalScrollBar()->setPageStep(viewport()->width());
+ horizontalScrollBar()
+ ->setRange(0, std::max(0, d->documentContainer.document()->width() - viewport()->width()));
+ verticalScrollBar()->setPageStep(viewport()->height());
+ verticalScrollBar()->setRange(0,
+ std::max(0,
+ d->documentContainer.document()->height()
+ - viewport()->height()));
+ viewport()->update();
+}
+
+QPoint QLiteHtmlWidget::scrollPosition() const
+{
+ return {horizontalScrollBar()->value(), verticalScrollBar()->value()};
+}
+
+void QLiteHtmlWidget::htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const
+{
+ *viewportPos = viewport()->mapFromParent(pos);
+ *htmlPos = *viewportPos + scrollPosition();
+}
diff --git a/src/plugins/help/qlitehtml/qlitehtmlwidget.h b/src/plugins/help/qlitehtml/qlitehtmlwidget.h
new file mode 100644
index 0000000000..433e65e15d
--- /dev/null
+++ b/src/plugins/help/qlitehtml/qlitehtmlwidget.h
@@ -0,0 +1,74 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of QLiteHtml.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QAbstractScrollArea>
+
+#include <functional>
+
+class QLiteHtmlWidgetPrivate;
+
+class QLiteHtmlWidget : public QAbstractScrollArea
+{
+ Q_OBJECT
+public:
+ explicit QLiteHtmlWidget(QWidget *parent = nullptr);
+ ~QLiteHtmlWidget() override;
+
+ void setUrl(const QUrl &url);
+ QUrl url() const;
+ void setHtml(const QString &content);
+ QString title() const;
+
+ void setDefaultFont(const QFont &font);
+ QFont defaultFont() const;
+
+ void scrollToAnchor(const QString &name);
+
+ using ResourceHandler = std::function<QByteArray(QUrl)>;
+ void setResourceHandler(const ResourceHandler &handler);
+
+ QString selectedText() const;
+
+signals:
+ void linkClicked(const QUrl &url);
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+ void resizeEvent(QResizeEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+ void mouseDoubleClickEvent(QMouseEvent *event) override;
+ void leaveEvent(QEvent *event) override;
+
+private:
+ void render();
+ QPoint scrollPosition() const;
+ void htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const;
+
+ QLiteHtmlWidgetPrivate *d;
+};