diff options
author | Eike Ziller <eike.ziller@qt.io> | 2019-07-29 15:30:58 +0200 |
---|---|---|
committer | Eike Ziller <eike.ziller@qt.io> | 2019-08-28 08:06:20 +0000 |
commit | 0efd65e07eb5090d50027ddec3d93960a7e075ac (patch) | |
tree | 1628743b3fe50199b0403cc4e3dab002c07d2249 /src/plugins/help | |
parent | 7855c9bb806d4d33561c713b9f7b1d3259995946 (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.txt | 13 | ||||
-rw-r--r-- | src/plugins/help/help.pro | 6 | ||||
-rw-r--r-- | src/plugins/help/helpplugin.cpp | 12 | ||||
-rw-r--r-- | src/plugins/help/litehtmlhelpviewer.cpp | 356 | ||||
-rw-r--r-- | src/plugins/help/litehtmlhelpviewer.h | 99 | ||||
-rw-r--r-- | src/plugins/help/qlitehtml/CMakeLists.txt | 19 | ||||
-rw-r--r-- | src/plugins/help/qlitehtml/README.md | 24 | ||||
-rw-r--r-- | src/plugins/help/qlitehtml/container_qpainter.cpp | 1086 | ||||
-rw-r--r-- | src/plugins/help/qlitehtml/container_qpainter.h | 181 | ||||
-rw-r--r-- | src/plugins/help/qlitehtml/qlitehtml.pri | 13 | ||||
-rw-r--r-- | src/plugins/help/qlitehtml/qlitehtmlwidget.cpp | 574 | ||||
-rw-r--r-- | src/plugins/help/qlitehtml/qlitehtmlwidget.h | 74 |
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; +}; |