diff options
author | Eike Ziller <eike.ziller@qt.io> | 2021-01-14 09:27:26 +0100 |
---|---|---|
committer | Eike Ziller <eike.ziller@qt.io> | 2021-01-19 12:33:49 +0100 |
commit | 6c5ef81184d0310fea1da1f2fd188fee6cdcd005 (patch) | |
tree | a3b72db74411b62c9efde9e175c90bc4c6969106 /src |
Initial import from Qt Creator
Diffstat (limited to 'src')
m--------- | src/3rdparty/litehtml | 0 | ||||
-rw-r--r-- | src/CMakeLists.txt | 119 | ||||
-rw-r--r-- | src/container_qpainter.cpp | 1360 | ||||
-rw-r--r-- | src/container_qpainter.h | 124 | ||||
-rw-r--r-- | src/container_qpainter_p.h | 163 | ||||
-rw-r--r-- | src/qlitehtml.pri | 164 | ||||
-rw-r--r-- | src/qlitehtml.qbs | 216 | ||||
-rw-r--r-- | src/qlitehtml_global.h | 36 | ||||
-rw-r--r-- | src/qlitehtmlwidget.cpp | 654 | ||||
-rw-r--r-- | src/qlitehtmlwidget.h | 95 |
10 files changed, 2931 insertions, 0 deletions
diff --git a/src/3rdparty/litehtml b/src/3rdparty/litehtml new file mode 160000 +Subproject db7f59d5886fd50f84d48720c79dc2e6152efa8 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..22f51eb --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,119 @@ +# set the following variables before adding as subdirectory into a project +# QLITEHTML_BIN_PATH - relative install path for result DLLs +# QLITEHTML_LIBRARY_PATH - relative install path for result dynamic libraries +# QLITEHTML_EXPORT - export name for qlitehtml +# QLITEHTML_DEVEL_COMPONENT - component name for development installation +# QLITEHTML_DEVEL_EXCLUDE_FROM_ALL - if development component should not be installed by default +# QLITEHTML_HEADER_PATH - relative install path for development headers +set(QLITEHTML_VERSION ${PROJECT_VERSION}) +set(QLITEHTML_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(QLITEHTML_VERSION_COMPAT ${QLITEHTML_VERSION} CACHE STRING "qlitehtml compat version number.") + +find_package(litehtml QUIET) +if(NOT TARGET litehtml AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/litehtml/CMakeLists.txt) + set(ORIG_FPIC ${CMAKE_POSITION_INDEPENDENT_CODE}) + if (WIN32) + set(LITEHTML_UTF8 ON CACHE BOOL "") + endif() + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + + add_subdirectory(3rdparty/litehtml EXCLUDE_FROM_ALL) + + set(CMAKE_POSITION_INDEPENDENT_CODE "${ORIG_FPIC}") + # force optimized litehtml even in debug + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + # except for windows + if (NOT WIN32) + target_compile_options(gumbo PRIVATE -O2) + target_compile_options(litehtml PRIVATE -O2) + endif() + endif() +endif() + +# TODO error if litehtml was not found? + +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +if(TARGET litehtml) + set(PUBLIC_HEADERS + container_qpainter.h + container_qpainter_p.h + qlitehtml_global.h + qlitehtmlwidget.h) + add_library(qlitehtml SHARED ${PUBLIC_HEADERS} container_qpainter.cpp qlitehtmlwidget.cpp) + + target_include_directories(qlitehtml PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>) + target_link_libraries(qlitehtml PUBLIC Qt5::Widgets PRIVATE litehtml) + target_compile_definitions(qlitehtml PRIVATE + QLITEHTML_LIBRARY + QT_NO_JAVA_STYLE_ITERATORS + QT_NO_CAST_TO_ASCII QT_RESTRICTED_CAST_FROM_ASCII + QT_USE_QSTRINGBUILDER) + if (WIN32) + target_compile_definitions(qlitehtml PRIVATE UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS) + if (NOT BUILD_WITH_PCH) + # Windows 8 0x0602 + target_compile_definitions(qlitehtml PRIVATE WINVER=0x0602 _WIN32_WINNT=0x0602 + WIN32_LEAN_AND_MEAN) + endif() + endif() + + set_target_properties(qlitehtml PROPERTIES + SOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}" + VERSION "${QLITEHTML_VERSION}" + SOVERSION "${QLITEHTML_VERSION_MAJOR}" + MACHO_CURRENT_VERSION "${QLITEHTML_VERSION}" + MACHO_COMPATIBILITY_VERSION "${QLITEHTML_VERSION_COMPAT}" + CXX_EXTENSIONS OFF + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + POSITION_INDEPENDENT_CODE ON) + + if(WIN32) + set_target_properties(qlitehtml PROPERTIES + SUFFIX "${QLITEHTML_VERSION_MAJOR}${CMAKE_SHARED_LIBRARY_SUFFIX}" + PREFIX "") + endif() + + if(DEFINED QLITEHTML_BIN_PATH AND DEFINED QLITEHTML_LIBRARY_PATH) + install(TARGETS qlitehtml + RUNTIME + DESTINATION "${QLITEHTML_BIN_PATH}" + OPTIONAL + LIBRARY + DESTINATION "${QLITEHTML_LIBRARY_PATH}" + NAMELINK_SKIP + OPTIONAL) + endif() + + if(DEFINED QLITEHTML_EXPORT) + install(TARGETS qlitehtml EXPORT ${QLITEHTML_EXPORT}) + add_library(${QLITEHTML_EXPORT}::qlitehtml ALIAS qlitehtml) + endif() + + if(DEFINED QLITEHTML_DEVEL_COMPONENT) + set(_EXCLUDE) + if(QLITEHTML_DEVEL_EXCLUDE_FROM_ALL) + set(_EXCLUDE EXCLUDE_FROM_ALL) + endif() + if(DEFINED QLITEHTML_LIBRARY_PATH) + install(TARGETS qlitehtml + OBJECTS + DESTINATION "${QLITEHTML_LIBRARY_PATH}" + COMPONENT ${QLITEHTML_DEVEL_COMPONENT} ${_EXCLUDE} + OPTIONAL + ARCHIVE + DESTINATION "${QLITEHTML_LIBRARY_PATH}" + COMPONENT ${QLITEHTML_DEVEL_COMPONENT} ${_EXCLUDE} + OPTIONAL) + endif() + if(DEFINED QLITEHTML_HEADER_PATH) + install( + FILES ${PUBLIC_HEADERS} + DESTINATION ${QLITEHTML_HEADER_PATH} + COMPONENT ${QLITEHTML_DEVEL_COMPONENT} ${_EXCLUDE}) + target_include_directories(qlitehtml PUBLIC $<INSTALL_INTERFACE:${QLITEHTML_HEADER_PATH}>) + endif() + endif() + +endif() diff --git a/src/container_qpainter.cpp b/src/container_qpainter.cpp new file mode 100644 index 0000000..269b0de --- /dev/null +++ b/src/container_qpainter.cpp @@ -0,0 +1,1360 @@ +/**************************************************************************** +** +** 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 +** 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 "container_qpainter_p.h" + +#include <QClipboard> +#include <QCursor> +#include <QDebug> +#include <QDir> +#include <QFont> +#include <QFontDatabase> +#include <QFontMetrics> +#include <QGuiApplication> +#include <QLoggingCategory> +#include <QPainter> +#include <QPalette> +#include <QRegularExpression> +#include <QScreen> +#include <QTextLayout> +#include <QUrl> + +#include <algorithm> +#include <set> + +const int kDragDistance = 5; + +using Font = QFont; +using Context = QPainter; + +namespace { +static 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; +} + +// 1) stops right away if element == stop, otherwise stops whenever stop element is encountered +// 2) moves down the first children from element until there is none anymore +static litehtml::element::ptr firstLeaf(const litehtml::element::ptr &element, + const litehtml::element::ptr &stop) +{ + if (element == stop) + return element; + litehtml::element::ptr current = element; + while (current != stop && current->get_children_count() > 0) + current = current->get_child(0); + return current; +} + +// 1) stops right away if element == stop, otherwise stops whenever stop element is encountered +// 2) starts at next sibling (up the hierarchy chain) if possible, otherwise root +// 3) returns first leaf of the element found in 2 +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); + } + return firstLeaf(current, stop); +} + +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, int(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. +// Qt5: 50 == normal, 75 == bold +// Qt6: == CSS +static QFont::Weight cssWeightToQtWeight(int cssWeight) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return QFont::Weight(cssWeight); +#else + if (cssWeight <= 400) + return QFont::Weight(cssWeight * 50 / 400); + if (cssWeight >= 700) + return QFont::Weight(75 + (cssWeight - 700) * 25 / 300); + return QFont::Weight(50 + (cssWeight - 400) * 25 / 300); +#endif +} + +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(); + } + QClipboard *cb = QGuiApplication::clipboard(); + if (cb->supportsSelection()) + cb->setText(text, QClipboard::Selection); +} + +QRect Selection::boundingRect() const +{ + QRect rect; + for (const QRect &r : selection) + rect = rect.united(r); + return rect; +} + +DocumentContainer::DocumentContainer() + : d(new DocumentContainerPrivate) +{} + +DocumentContainer::~DocumentContainer() = default; + +litehtml::uint_ptr DocumentContainerPrivate::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(',', Qt::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 + struct CompareCaseinsensitive + { + bool operator()(const QString &a, const QString &b) const + { + return a.compare(b, Qt::CaseInsensitive) < 0; + } + }; + static const QStringList known = QFontDatabase().families(); + static const std::set<QString, CompareCaseinsensitive> knownFamilies(known.cbegin(), + known.cend()); + font->setFamily(familyNames.last()); + for (const QString &name : qAsConst(familyNames)) { + const auto found = knownFamilies.find(name); + if (found != knownFamilies.end()) { + font->setFamily(*found); + break; + } + } +#endif + font->setPixelSize(size); + font->setWeight(cssWeightToQtWeight(weight)); + font->setStyle(toQFontStyle(italic)); + if (decoration == litehtml::font_decoration_underline) + font->setUnderline(true); + if (decoration == litehtml::font_decoration_overline) + font->setOverline(true); + if (decoration == litehtml::font_decoration_linethrough) + font->setStrikeOut(true); + 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 DocumentContainerPrivate::delete_font(litehtml::uint_ptr hFont) +{ + auto font = reinterpret_cast<Font *>(hFont); + delete font; +} + +int DocumentContainerPrivate::text_width(const litehtml::tchar_t *text, litehtml::uint_ptr hFont) +{ + const QFontMetrics fm(toQFont(hFont)); + return fm.horizontalAdvance(QString::fromUtf8(text)); +} + +void DocumentContainerPrivate::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 DocumentContainerPrivate::pt_to_px(int pt) +{ + // magic factor of 11/12 to account for differences to webengine/webkit + return m_paintDevice->physicalDpiY() * pt * 11 / m_paintDevice->logicalDpiY() / 12; +} + +int DocumentContainerPrivate::get_default_font_size() const +{ + return m_defaultFont.pointSize(); +} + +const litehtml::tchar_t *DocumentContainerPrivate::get_default_font_name() const +{ + return m_defaultFontFamilyName.constData(); +} + +void DocumentContainerPrivate::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 DocumentContainerPrivate::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 DocumentContainerPrivate::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 DocumentContainerPrivate::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(); +} + +static QString tagName(const litehtml::element::ptr &e) +{ + litehtml::element::ptr current = e; + while (current && std::strlen(current->get_tagName()) == 0) + current = current->parent(); + return current ? QString::fromUtf8(current->get_tagName()) : QString(); +} + +void DocumentContainerPrivate::buildIndex() +{ + m_index.elementToIndex.clear(); + m_index.indexToElement.clear(); + m_index.text.clear(); + + int index = 0; + bool inBody = false; + litehtml::element::ptr current = firstLeaf(m_document->root(), nullptr); + while (current != m_document->root()) { + m_index.elementToIndex.insert({current, index}); + if (!inBody) + inBody = tagName(current).toLower() == "body"; + if (inBody && current->is_visible()) { + litehtml::tstring text; + current->get_text(text); + if (!text.empty()) { + m_index.indexToElement.push_back({index, current}); + const QString str = QString::fromStdString(text); + m_index.text += str; + index += str.size(); + } + } + current = nextLeaf(current, m_document->root()); + } +} + +void DocumentContainerPrivate::draw_background(litehtml::uint_ptr hdc, + const litehtml::background_paint &bg) +{ + auto painter = toQPainter(hdc); + if (bg.is_root) { + // TODO ? + 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 DocumentContainerPrivate::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 DocumentContainerPrivate::set_caption(const litehtml::tchar_t *caption) +{ + m_caption = QString::fromUtf8(caption); +} + +void DocumentContainerPrivate::set_base_url(const litehtml::tchar_t *base_url) +{ + m_baseUrl = QString::fromUtf8(base_url); +} + +void DocumentContainerPrivate::link(const std::shared_ptr<litehtml::document> &doc, + const litehtml::element::ptr &el) +{ + // TODO + qDebug(log) << "link"; + Q_UNUSED(doc) + Q_UNUSED(el) +} + +void DocumentContainerPrivate::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 DocumentContainerPrivate::set_cursor(const litehtml::tchar_t *cursor) +{ + m_cursorCallback(toQCursor(QString::fromUtf8(cursor))); +} + +void DocumentContainerPrivate::transform_text(litehtml::tstring &text, litehtml::text_transform tt) +{ + // TODO + qDebug(log) << "transform_text"; + Q_UNUSED(text) + Q_UNUSED(tt) +} + +void DocumentContainerPrivate::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 DocumentContainerPrivate::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 DocumentContainerPrivate::del_clip() +{ + // TODO + qDebug(log) << "del_clip"; +} + +void DocumentContainerPrivate::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> DocumentContainerPrivate::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 DocumentContainerPrivate::get_media_features(litehtml::media_features &media) const +{ + media.type = litehtml::media_type_screen; + // TODO + qDebug(log) << "get_media_features"; +} + +void DocumentContainerPrivate::get_language(litehtml::tstring &language, litehtml::tstring &culture) const +{ + // TODO + qDebug(log) << "get_language"; + Q_UNUSED(language) + Q_UNUSED(culture) +} + +void DocumentContainer::setPaintDevice(QPaintDevice *paintDevice) +{ + d->m_paintDevice = paintDevice; +} + +void DocumentContainer::setScrollPosition(const QPoint &pos) +{ + d->m_scrollPosition = pos; +} + +void DocumentContainer::setDocument(const QByteArray &data, DocumentContainerContext *context) +{ + d->m_pixmaps.clear(); + d->m_selection = {}; + d->m_document = litehtml::document::createFromUTF8(data.constData(), d.get(), &context->d->context); + d->buildIndex(); +} + +bool DocumentContainer::hasDocument() const +{ + return d->m_document.get(); +} + +void DocumentContainer::setBaseUrl(const QString &url) +{ + d->set_base_url(url.toUtf8().constData()); +} + +void DocumentContainer::render(int width, int height) +{ + d->m_clientRect = {0, 0, width, height}; + if (!d->m_document) + return; + d->m_document->render(width); + d->m_selection.update(); +} + +void DocumentContainer::draw(QPainter *painter, const QRect &clip) +{ + d->drawSelection(painter, clip); + const QPoint pos = -d->m_scrollPosition; + const litehtml::position clipRect = {clip.x(), clip.y(), clip.width(), clip.height()}; + d->m_document->draw(reinterpret_cast<litehtml::uint_ptr>(painter), pos.x(), pos.y(), &clipRect); +} + +int DocumentContainer::documentWidth() const +{ + return d->m_document->width(); +} + +int DocumentContainer::documentHeight() const +{ + return d->m_document->height(); +} + +int DocumentContainer::anchorY(const QString &anchorName) const +{ + litehtml::element::ptr element = d->m_document->root()->select_one( + QString("#%1").arg(anchorName).toStdString()); + if (!element) { + element = d->m_document->root()->select_one(QString("[name=%1]").arg(anchorName).toStdString()); + } + if (element) + return element->get_placement().y; + return -1; +} + +QVector<QRect> DocumentContainer::mousePressEvent(const QPoint &documentPos, + const QPoint &viewportPos, + Qt::MouseButton button) +{ + if (!d->m_document || button != Qt::LeftButton) + return {}; + QVector<QRect> redrawRects; + // selection + if (d->m_selection.isValid()) + redrawRects.append(d->m_selection.boundingRect()); + d->m_selection = {}; + d->m_selection.selectionStartDocumentPos = documentPos; + d->m_selection.startElem = deepest_child_at_point(d->m_document, + documentPos, + viewportPos, + d->m_selection.mode); + // post to litehtml + litehtml::position::vector redrawBoxes; + if (d->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 (!d->m_document) + return {}; + QVector<QRect> redrawRects; + // selection + if (d->m_selection.isSelecting + || (!d->m_selection.selectionStartDocumentPos.isNull() + && (d->m_selection.selectionStartDocumentPos - documentPos).manhattanLength() >= kDragDistance + && d->m_selection.startElem.element)) { + const Selection::Element element = deepest_child_at_point(d->m_document, + documentPos, + viewportPos, + d->m_selection.mode); + if (element.element) { + redrawRects.append( + d->m_selection.boundingRect() /*.adjusted(-1, -1, +1, +1)*/); // redraw old selection area + d->m_selection.endElem = element; + d->m_selection.update(); + redrawRects.append(d->m_selection.boundingRect()); + } + d->m_selection.isSelecting = true; + } + litehtml::position::vector redrawBoxes; + if (d->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 (!d->m_document || button != Qt::LeftButton) + return {}; + QVector<QRect> redrawRects; + // selection + d->m_selection.isSelecting = false; + d->m_selection.selectionStartDocumentPos = {}; + if (d->m_selection.isValid()) + d->m_blockLinks = true; + else + d->m_selection = {}; + litehtml::position::vector redrawBoxes; + if (d->m_document->on_lbutton_up( + documentPos.x(), documentPos.y(), viewportPos.x(), viewportPos.y(), redrawBoxes)) { + for (const litehtml::position &box : redrawBoxes) + redrawRects.append(toQRect(box)); + } + d->m_blockLinks = false; + return redrawRects; +} + +QVector<QRect> DocumentContainer::mouseDoubleClickEvent(const QPoint &documentPos, + const QPoint &viewportPos, + Qt::MouseButton button) +{ + if (!d->m_document || button != Qt::LeftButton) + return {}; + QVector<QRect> redrawRects; + d->m_selection = {}; + d->m_selection.mode = Selection::Mode::Word; + const Selection::Element element = deepest_child_at_point(d->m_document, + documentPos, + viewportPos, + d->m_selection.mode); + if (element.element) { + d->m_selection.startElem = element; + d->m_selection.endElem = d->m_selection.startElem; + d->m_selection.isSelecting = true; + d->m_selection.update(); + if (d->m_selection.isValid()) + redrawRects.append(d->m_selection.boundingRect()); + } else { + if (d->m_selection.isValid()) + redrawRects.append(d->m_selection.boundingRect()); + d->m_selection = {}; + } + return redrawRects; +} + +QVector<QRect> DocumentContainer::leaveEvent() +{ + if (!d->m_document) + return {}; + litehtml::position::vector redrawBoxes; + if (d->m_document->on_mouse_leave(redrawBoxes)) { + QVector<QRect> redrawRects; + for (const litehtml::position &box : redrawBoxes) + redrawRects.append(toQRect(box)); + return redrawRects; + } + return {}; +} + +QUrl DocumentContainer::linkAt(const QPoint &documentPos, const QPoint &viewportPos) +{ + if (!d->m_document) + return {}; + const litehtml::element::ptr element = d->m_document->root()->get_element_by_point( + documentPos.x(), documentPos.y(), viewportPos.x(), viewportPos.y()); + const char *href = element->get_attr("href"); + if (href) + return d->resolveUrl(QString::fromUtf8(href), d->m_baseUrl); + return {}; +} + +QString DocumentContainer::caption() const +{ + return d->m_caption; +} + +QString DocumentContainer::selectedText() const +{ + return d->m_selection.text; +} + +void DocumentContainer::findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped, + bool *success, + QVector<QRect> *oldSelection, + QVector<QRect> *newSelection) +{ + if (success) + *success = false; + if (oldSelection) + oldSelection->clear(); + if (newSelection) + newSelection->clear(); + if (!d->m_document) + return; + const bool backward = flags & QTextDocument::FindBackward; + int startIndex = backward ? -1 : 0; + if (d->m_selection.startElem.element && d->m_selection.endElem.element) { // selection + // poor-man's incremental search starts at beginning of selection, + // non-incremental at end (forward search) or beginning (backward search) + Selection::Element start; + Selection::Element end; + std::tie(start, end) = getStartAndEnd(d->m_selection.startElem, d->m_selection.endElem); + Selection::Element searchStart; + if (incremental || backward) { + if (start.index < 0) // fully selected + searchStart = {firstLeaf(start.element, nullptr), 0, -1}; + else + searchStart = start; + } else { + if (end.index < 0) // fully selected + searchStart = {nextLeaf(end.element, nullptr), 0, -1}; + else + searchStart = end; + } + const auto findInIndex = d->m_index.elementToIndex.find(searchStart.element); + if (findInIndex == std::end(d->m_index.elementToIndex)) { + qWarning() << "internal error: cannot find litehmtl element in index"; + return; + } + startIndex = findInIndex->second + searchStart.index; + if (backward) + --startIndex; + } + + const auto fillXPos = [](const Selection::Element &e) { + litehtml::tstring ttext; + e.element->get_text(ttext); + const QString text = QString::fromStdString(ttext); + const QFont &font = toQFont(e.element->get_font()); + const QFontMetrics fm(font); + return Selection::Element{e.element, e.index, fm.size(0, text.left(e.index)).width()}; + }; + + QString term = QRegularExpression::escape(text); + if (flags & QTextDocument::FindWholeWords) + term = QString("\\b%1\\b").arg(term); + const QRegularExpression::PatternOptions patternOptions + = (flags & QTextDocument::FindCaseSensitively) ? QRegularExpression::NoPatternOption + : QRegularExpression::CaseInsensitiveOption; + const QRegularExpression expression(term, patternOptions); + + int foundIndex = backward ? d->m_index.text.lastIndexOf(expression, startIndex) + : d->m_index.text.indexOf(expression, startIndex); + if (foundIndex < 0) { // wrap + foundIndex = backward ? d->m_index.text.lastIndexOf(expression) + : d->m_index.text.indexOf(expression); + if (wrapped && foundIndex >= 0) + *wrapped = true; + } + if (foundIndex >= 0) { + const Index::Entry startEntry = d->m_index.findElement(foundIndex); + const Index::Entry endEntry = d->m_index.findElement(foundIndex + text.size()); + if (!startEntry.second || !endEntry.second) { + qWarning() << "internal error: search ended up with nullptr elements"; + return; + } + if (oldSelection) + *oldSelection = d->m_selection.selection; + d->m_selection = {}; + d->m_selection.startElem = fillXPos({startEntry.second, foundIndex - startEntry.first, -1}); + d->m_selection.endElem = fillXPos( + {endEntry.second, int(foundIndex + text.size() - endEntry.first), -1}); + d->m_selection.update(); + if (newSelection) + *newSelection = d->m_selection.selection; + if (success) + *success = true; + return; + } + return; +} + +void DocumentContainer::setDefaultFont(const QFont &font) +{ + d->m_defaultFont = font; + d->m_defaultFontFamilyName = d->m_defaultFont.family().toUtf8(); +} + +QFont DocumentContainer::defaultFont() const +{ + return d->m_defaultFont; +} + +void DocumentContainer::setDataCallback(const DocumentContainer::DataCallback &callback) +{ + d->m_dataCallback = callback; +} + +void DocumentContainer::setCursorCallback(const DocumentContainer::CursorCallback &callback) +{ + d->m_cursorCallback = callback; +} + +void DocumentContainer::setLinkCallback(const DocumentContainer::LinkCallback &callback) +{ + d->m_linkCallback = callback; +} + +void DocumentContainer::setPaletteCallback(const DocumentContainer::PaletteCallback &callback) +{ + d->m_paletteCallback = callback; +} + +static litehtml::element::ptr elementForY(int y, const litehtml::document::ptr &document) +{ + if (!document) + return {}; + + const std::function<litehtml::element::ptr(int, litehtml::element::ptr)> recursion = + [&recursion](int y, const litehtml::element::ptr &element) { + litehtml::element::ptr result; + const int subY = y - element->get_position().y; + if (subY <= 0) + return element; + for (int i = 0; i < int(element->get_children_count()); ++i) { + const litehtml::element::ptr child = element->get_child(i); + result = recursion(subY, child); + if (result) + return result; + } + return result; + }; + + return recursion(y, document->root()); +} + +int DocumentContainer::withFixedElementPosition(int y, const std::function<void()> &action) +{ + const litehtml::element::ptr element = elementForY(y, d->m_document); + action(); + if (element) + return element->get_placement().y; + return -1; +} + +QPixmap DocumentContainerPrivate::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 DocumentContainerPrivate::serifFont() const +{ + // TODO make configurable + return {"Times New Roman"}; +} + +QString DocumentContainerPrivate::sansSerifFont() const +{ + // TODO make configurable + return {"Arial"}; +} + +QString DocumentContainerPrivate::monospaceFont() const +{ + // TODO make configurable + return {"Courier"}; +} + +QUrl DocumentContainerPrivate::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; +} + +Index::Entry Index::findElement(int index) const +{ + const auto upper = std::upper_bound(std::begin(indexToElement), + std::end(indexToElement), + Entry{index, {}}, + [](const Entry &a, const Entry &b) { + return a.first < b.first; + }); + if (upper == std::begin(indexToElement)) // should not happen for index >= 0 + return {-1, {}}; + return *(upper - 1); +} + +DocumentContainerContext::DocumentContainerContext() + : d(new DocumentContainerContextPrivate) +{} + +DocumentContainerContext::~DocumentContainerContext() = default; + +void DocumentContainerContext::setMasterStyleSheet(const QString &css) +{ + d->context.load_master_stylesheet(css.toUtf8().constData()); +} diff --git a/src/container_qpainter.h b/src/container_qpainter.h new file mode 100644 index 0000000..66b10c2 --- /dev/null +++ b/src/container_qpainter.h @@ -0,0 +1,124 @@ +/**************************************************************************** +** +** 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 +** 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 "qlitehtml_global.h" + +#include <QByteArray> +#include <QPaintDevice> +#include <QPainter> +#include <QPoint> +#include <QRect> +#include <QString> +#include <QTextDocument> +#include <QUrl> +#include <QVector> + +#include <functional> +#include <memory> + +class DocumentContainerPrivate; +class DocumentContainerContextPrivate; + +class QLITEHTML_EXPORT DocumentContainerContext +{ +public: + DocumentContainerContext(); + ~DocumentContainerContext(); + + void setMasterStyleSheet(const QString &css); + +private: + std::unique_ptr<DocumentContainerContextPrivate> d; + + friend class DocumentContainer; + friend class DocumentContainerPrivate; +}; + +class QLITEHTML_EXPORT DocumentContainer +{ +public: + DocumentContainer(); + virtual ~DocumentContainer(); + +public: // outside API + void setPaintDevice(QPaintDevice *paintDevice); + void setDocument(const QByteArray &data, DocumentContainerContext *context); + bool hasDocument() const; + void setBaseUrl(const QString &url); + void setScrollPosition(const QPoint &pos); + void render(int width, int height); + void draw(QPainter *painter, const QRect &clip); + int documentWidth() const; + int documentHeight() const; + int anchorY(const QString &anchorName) const; + + // 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(); + + QUrl linkAt(const QPoint &documentPos, const QPoint &viewportPos); + + QString caption() const; + QString selectedText() const; + + void findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped, + bool *success, + QVector<QRect> *oldSelection, + QVector<QRect> *newSelection); + + 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); + + int withFixedElementPosition(int y, const std::function<void()> &action); + +private: + std::unique_ptr<DocumentContainerPrivate> d; +}; diff --git a/src/container_qpainter_p.h b/src/container_qpainter_p.h new file mode 100644 index 0000000..0391147 --- /dev/null +++ b/src/container_qpainter_p.h @@ -0,0 +1,163 @@ +/**************************************************************************** +** +** Copyright (C) 2020 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 +** 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 "container_qpainter.h" + +#include <litehtml.h> + +#include <QPaintDevice> +#include <QPixmap> +#include <QPoint> +#include <QRect> +#include <QString> +#include <QVector> + +#include <unordered_map> + +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; +}; + +struct Index +{ + QString text; + // only contains leaf elements + std::unordered_map<litehtml::element::ptr, int> elementToIndex; + + using Entry = std::pair<int, litehtml::element::ptr>; + std::vector<Entry> indexToElement; + + Entry findElement(int index) const; +}; + +class DocumentContainerPrivate : public litehtml::document_container +{ +public: // document_container API + 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; + + 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; + void buildIndex(); + + QPaintDevice *m_paintDevice = nullptr; + litehtml::document::ptr m_document; + Index m_index; + 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; + DocumentContainer::DataCallback m_dataCallback; + DocumentContainer::CursorCallback m_cursorCallback; + DocumentContainer::LinkCallback m_linkCallback; + DocumentContainer::PaletteCallback m_paletteCallback; + bool m_blockLinks = false; +}; + +class DocumentContainerContextPrivate +{ +public: + litehtml::context context; +}; diff --git a/src/qlitehtml.pri b/src/qlitehtml.pri new file mode 100644 index 0000000..a4b6918 --- /dev/null +++ b/src/qlitehtml.pri @@ -0,0 +1,164 @@ +exists($$PWD/3rdparty/litehtml/CMakeLists.txt) { + LH_SRC = $$PWD/3rdparty/litehtml + LH_HDR = $$LH_SRC/include/litehtml + GB_SRC = $$PWD/3rdparty/litehtml/src/gumbo + GB_HDR = $$GB_SRC/include/gumbo + + # gumbo + SOURCES += \ + $$GB_SRC/attribute.c \ + $$GB_SRC/char_ref.c \ + $$GB_SRC/error.c \ + $$GB_SRC/parser.c \ + $$GB_SRC/string_buffer.c \ + $$GB_SRC/string_piece.c \ + $$GB_SRC/tag.c \ + $$GB_SRC/tokenizer.c \ + $$GB_SRC/utf8.c \ + $$GB_SRC/util.c \ + $$GB_SRC/vector.c + + HEADERS += \ + $$GB_SRC/include//gumbo.h \ + $$GB_HDR/attribute.h \ + $$GB_HDR/char_ref.h \ + $$GB_HDR/error.h \ + $$GB_HDR/insertion_mode.h \ + $$GB_HDR/parser.h \ + $$GB_HDR/string_buffer.h \ + $$GB_HDR/string_piece.h \ + $$GB_HDR/tag_enum.h \ + $$GB_HDR/tag_gperf.h \ + $$GB_HDR/tag_sizes.h \ + $$GB_HDR/tag_strings.h \ + $$GB_HDR/token_type.h \ + $$GB_HDR/tokenizer.h \ + $$GB_HDR/tokenizer_states.h \ + $$GB_HDR/utf8.h \ + $$GB_HDR/util.h \ + $$GB_HDR/vector.h + + INCLUDEPATH *= $$GB_SRC/include $$GB_HDR + + win32 { + HEADERS += \ + $$GB_SRC/visualc/include/strings.h + INCLUDEPATH *= $$GB_SRC/visualc/include + } + + # litehtml + SOURCES += \ + $$LH_SRC/src/background.cpp \ + $$LH_SRC/src/box.cpp \ + $$LH_SRC/src/context.cpp \ + $$LH_SRC/src/css_length.cpp \ + $$LH_SRC/src/css_selector.cpp \ + $$LH_SRC/src/document.cpp \ + $$LH_SRC/src/el_anchor.cpp \ + $$LH_SRC/src/el_base.cpp \ + $$LH_SRC/src/el_before_after.cpp \ + $$LH_SRC/src/el_body.cpp \ + $$LH_SRC/src/el_break.cpp \ + $$LH_SRC/src/el_cdata.cpp \ + $$LH_SRC/src/el_comment.cpp \ + $$LH_SRC/src/el_div.cpp \ + $$LH_SRC/src/element.cpp \ + $$LH_SRC/src/el_font.cpp \ + $$LH_SRC/src/el_image.cpp \ + $$LH_SRC/src/el_li.cpp \ + $$LH_SRC/src/el_link.cpp \ + $$LH_SRC/src/el_para.cpp \ + $$LH_SRC/src/el_script.cpp \ + $$LH_SRC/src/el_space.cpp \ + $$LH_SRC/src/el_style.cpp \ + $$LH_SRC/src/el_table.cpp \ + $$LH_SRC/src/el_td.cpp \ + $$LH_SRC/src/el_text.cpp \ + $$LH_SRC/src/el_title.cpp \ + $$LH_SRC/src/el_tr.cpp \ + $$LH_SRC/src/html.cpp \ + $$LH_SRC/src/html_tag.cpp \ + $$LH_SRC/src/iterators.cpp \ + $$LH_SRC/src/media_query.cpp \ + $$LH_SRC/src/num_cvt.cpp \ + $$LH_SRC/src/style.cpp \ + $$LH_SRC/src/stylesheet.cpp \ + $$LH_SRC/src/table.cpp \ + $$LH_SRC/src/utf8_strings.cpp \ + $$LH_SRC/src/web_color.cpp + + HEADERS += \ + $$LH_SRC/include/litehtml.h \ + $$LH_HDR/attributes.h \ + $$LH_HDR/background.h \ + $$LH_HDR/borders.h \ + $$LH_HDR/box.h \ + $$LH_HDR/context.h \ + $$LH_HDR/css_length.h \ + $$LH_HDR/css_margins.h \ + $$LH_HDR/css_offsets.h \ + $$LH_HDR/css_position.h \ + $$LH_HDR/css_selector.h \ + $$LH_HDR/document.h \ + $$LH_HDR/el_anchor.h \ + $$LH_HDR/el_base.h \ + $$LH_HDR/el_before_after.h \ + $$LH_HDR/el_body.h \ + $$LH_HDR/el_break.h \ + $$LH_HDR/el_cdata.h \ + $$LH_HDR/el_comment.h \ + $$LH_HDR/el_div.h \ + $$LH_HDR/el_font.h \ + $$LH_HDR/el_image.h \ + $$LH_HDR/el_li.h \ + $$LH_HDR/el_link.h \ + $$LH_HDR/el_para.h \ + $$LH_HDR/el_script.h \ + $$LH_HDR/el_space.h \ + $$LH_HDR/el_style.h \ + $$LH_HDR/el_table.h \ + $$LH_HDR/el_td.h \ + $$LH_HDR/el_text.h \ + $$LH_HDR/el_title.h \ + $$LH_HDR/el_tr.h \ + $$LH_HDR/element.h \ + $$LH_HDR/html.h \ + $$LH_HDR/html_tag.h \ + $$LH_HDR/iterators.h \ + $$LH_HDR/media_query.h \ + $$LH_HDR/num_cvt.h \ + $$LH_HDR/os_types.h \ + $$LH_HDR/style.h \ + $$LH_HDR/stylesheet.h \ + $$LH_HDR/table.h \ + $$LH_HDR/types.h \ + $$LH_HDR/utf8_strings.h \ + $$LH_HDR/web_color.h + + INCLUDEPATH *= $$LH_SRC/include $$LH_HDR + + # litehtml without optimization is not fun + QMAKE_CFLAGS_DEBUG += -O2 + QMAKE_CXXFLAGS_DEBUG += -O2 +} else { + INCLUDEPATH *= $$LITEHTML_INSTALL_DIR/include $$LITEHTML_INSTALL_DIR/include/litehtml + LITEHTML_LIB_DIR = $$LITEHTML_INSTALL_DIR/lib + LIBS += -L$$LITEHTML_LIB_DIR -llitehtml -lgumbo + + win32: PRE_TARGETDEPS += $$LITEHTML_LIB_DIR/litehtml.lib $$LITEHTML_LIB_DIR/gumbo.lib + else:unix: PRE_TARGETDEPS += $$LITEHTML_LIB_DIR/liblitehtml.a $$LITEHTML_LIB_DIR/libgumbo.a +} + +HEADERS += \ + $$PWD/container_qpainter.h \ + $$PWD/container_qpainter_p.h \ + $$PWD/qlitehtmlwidget.h + +SOURCES += \ + $$PWD/container_qpainter.cpp \ + $$PWD/qlitehtmlwidget.cpp + +INCLUDEPATH *= $$PWD +win32: DEFINES += LITEHTML_UTF8 + +DEFINES *= QLITEHTML_STATIC_LIBRARY diff --git a/src/qlitehtml.qbs b/src/qlitehtml.qbs new file mode 100644 index 0000000..1e06f77 --- /dev/null +++ b/src/qlitehtml.qbs @@ -0,0 +1,216 @@ +import qbs.File +import qbs.FileInfo + +Product { + type: buildLib ? ["staticlibrary"] : undefined + + Depends { name: "cpp" } + Depends { name: "qtc" } + + property bool useExternalLib: qtc.litehtmlInstallDir + property bool buildLib: !useExternalLib && File.exists(path + "/3rdparty/litehtml/CMakeLists.txt") + condition: useExternalLib || buildLib + + property string gumboSrcDir: path + "/3rdparty/litehtml/src/gumbo" + property string gumboHeaderDir: gumboSrcDir + "/include/gumbo" + property string litehtmlHeaderDir: path + "/3rdparty/litehtml/include/litehtml" + property string mainHeaderDir: litehtmlHeaderDir + '/..' + property stringList sharedDefines: { + var defines = ["QLITEHTML_STATIC_LIBRARY"]; + if (qbs.targetOS.contains("windows")) + defines.push("LITEHTML_UTF8"); + return defines; + } + + cpp.defines: sharedDefines + cpp.includePaths: { + var paths = [gumboHeaderDir, gumboHeaderDir + '/..', litehtmlHeaderDir, mainHeaderDir]; + if (qbs.targetOS.contains("windows")) + paths.push(gumboSrcDir + "/visualc/include"); + return paths; + } + cpp.optimization: "fast" + cpp.warningLevel: "none" + cpp.cxxLanguageVersion: "c++14" + + Export { + Depends { name: "cpp" } + Group { + name: "litehtml/Qt glue" + cpp.warningLevel: "none" + files: [ + "container_qpainter.cpp", + "container_qpainter.h", + "container_qpainter_p.h", + "qlitehtmlwidget.cpp", + "qlitehtmlwidget.h", + ] + } + + Properties { + condition: product.useExternalLib + cpp.dynamicLibraries: ["litehtml", "gumbo"] + cpp.includePaths: [ + FileInfo.joinPaths(qtc.litehtmlInstallDir, "include"), + FileInfo.joinPaths(qtc.litehtmlInstallDir, "include", "litehtml"), + ] + cpp.libraryPaths: FileInfo.joinPaths(qtc.litehtmlInstallDir, "lib") + } + Properties { + condition: product.buildLib + cpp.defines: product.sharedDefines + cpp.includePaths: [product.mainHeaderDir, path] + } + } + + Group { + condition: buildLib + name: "gumbo sources" + prefix: gumboSrcDir + '/' + files: [ + "attribute.c", + "char_ref.c", + "error.c", + "parser.c", + "string_buffer.c", + "string_piece.c", + "tag.c", + "tokenizer.c", + "utf8.c", + "util.c", + "vector.c", + ] + } + + Group { + condition: buildLib + name: "gumbo headers" + prefix: gumboHeaderDir + '/' + files: [ + "../gumbo.h", + "attribute.h", + "char_ref.h", + "error.h", + "insertion_mode.h", + "parser.h", + "string_buffer.h", + "string_piece.h", + "tag_enum.h", + "tag_gperf.h", + "tag_sizes.h", + "tag_strings.h", + "token_type.h", + "tokenizer.h", + "tokenizer_states.h", + "utf8.h", + "util.h", + "vector.h", + ] + + Group { + name: "gumbo Windows headers" + condition: qbs.targetOS.contains("windows") + files: "../../visualc/include/strings.h" + } + } + + Group { + condition: buildLib + name: "litehtml sources" + prefix: litehtmlHeaderDir + "/../../src/" + files: [ + "background.cpp", + "box.cpp", + "context.cpp", + "css_length.cpp", + "css_selector.cpp", + "document.cpp", + "el_anchor.cpp", + "el_base.cpp", + "el_before_after.cpp", + "el_body.cpp", + "el_break.cpp", + "el_cdata.cpp", + "el_comment.cpp", + "el_div.cpp", + "element.cpp", + "el_font.cpp", + "el_image.cpp", + "el_li.cpp", + "el_link.cpp", + "el_para.cpp", + "el_script.cpp", + "el_space.cpp", + "el_style.cpp", + "el_table.cpp", + "el_td.cpp", + "el_text.cpp", + "el_title.cpp", + "el_tr.cpp", + "html.cpp", + "html_tag.cpp", + "iterators.cpp", + "media_query.cpp", + "num_cvt.cpp", + "style.cpp", + "stylesheet.cpp", + "table.cpp", + "utf8_strings.cpp", + "web_color.cpp", + ] + } + + Group { + condition: buildLib + name: "litehtml headers" + prefix: litehtmlHeaderDir + '/' + files: [ + "../litehtml.h", + "attributes.h", + "background.h", + "borders.h", + "box.h", + "context.h", + "css_length.h", + "css_margins.h", + "css_offsets.h", + "css_position.h", + "css_selector.h", + "document.h", + "el_anchor.h", + "el_base.h", + "el_before_after.h", + "el_body.h", + "el_break.h", + "el_cdata.h", + "el_comment.h", + "el_div.h", + "el_font.h", + "el_image.h", + "el_li.h", + "el_link.h", + "el_para.h", + "el_script.h", + "el_space.h", + "el_style.h", + "el_table.h", + "el_td.h", + "el_text.h", + "el_title.h", + "el_tr.h", + "element.h", + "html.h", + "html_tag.h", + "iterators.h", + "media_query.h", + "num_cvt.h", + "os_types.h", + "style.h", + "stylesheet.h", + "table.h", + "types.h", + "utf8_strings.h", + "web_color.h", + ] + } +} diff --git a/src/qlitehtml_global.h b/src/qlitehtml_global.h new file mode 100644 index 0000000..5ff23c3 --- /dev/null +++ b/src/qlitehtml_global.h @@ -0,0 +1,36 @@ +/**************************************************************************** +** +** Copyright (C) 2020 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 +** 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 <qglobal.h> + +#if defined(QLITEHTML_LIBRARY) +# define QLITEHTML_EXPORT Q_DECL_EXPORT +#elif defined(QLITEHTML_STATIC_LIBRARY) // Abuse single files for manual tests +# define QLITEHTML_EXPORT +#else +# define QLITEHTML_EXPORT Q_DECL_IMPORT +#endif diff --git a/src/qlitehtmlwidget.cpp b/src/qlitehtmlwidget.cpp new file mode 100644 index 0000000..8110417 --- /dev/null +++ b/src/qlitehtmlwidget.cpp @@ -0,0 +1,654 @@ +/**************************************************************************** +** +** 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 +** 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> + +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: + QString html; + DocumentContainerContext context; + QUrl url; + DocumentContainer documentContainer; + qreal zoomFactor = 1; +}; + +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.setMasterStyleSheet(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.setBaseUrl(baseUrl); +} + +QUrl QLiteHtmlWidget::url() const +{ + return d->url; +} + +void QLiteHtmlWidget::setHtml(const QString &content) +{ + d->html = content; + d->documentContainer.setPaintDevice(viewport()); + d->documentContainer.setDocument(content.toUtf8(), &d->context); + verticalScrollBar()->setValue(0); + horizontalScrollBar()->setValue(0); + render(); +} + +QString QLiteHtmlWidget::html() const +{ + return d->html; +} + +QString QLiteHtmlWidget::title() const +{ + return d->documentContainer.caption(); +} + +void QLiteHtmlWidget::setZoomFactor(qreal scale) +{ + Q_ASSERT(scale != 0); + d->zoomFactor = scale; + withFixedTextPosition([this] { render(); }); +} + +qreal QLiteHtmlWidget::zoomFactor() const +{ + return d->zoomFactor; +} + +bool QLiteHtmlWidget::findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped) +{ + bool success = false; + QVector<QRect> oldSelection; + QVector<QRect> newSelection; + d->documentContainer + .findText(text, flags, incremental, wrapped, &success, &oldSelection, &newSelection); + // scroll to search result position and/or redraw as necessary + QRect newSelectionCombined; + for (const QRect &r : newSelection) + newSelectionCombined = newSelectionCombined.united(r); + QScrollBar *vBar = verticalScrollBar(); + const int top = newSelectionCombined.top(); + const int bottom = newSelectionCombined.bottom() - toVirtual(viewport()->size()).height(); + if (success && top < vBar->value() && vBar->minimum() <= top) { + vBar->setValue(top); + } else if (success && vBar->value() < bottom && bottom <= vBar->maximum()) { + vBar->setValue(bottom); + } else { + viewport()->update(fromVirtual(newSelectionCombined.translated(-scrollPosition()))); + for (const QRect &r : oldSelection) + viewport()->update(fromVirtual(r.translated(-scrollPosition()))); + } + return success; +} + +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.hasDocument()) + return; + horizontalScrollBar()->setValue(0); + if (name.isEmpty()) { + verticalScrollBar()->setValue(0); + return; + } + const int y = d->documentContainer.anchorY(name); + if (y >= 0) + 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.hasDocument()) + return; + d->documentContainer.setScrollPosition(scrollPosition()); + QPainter p(viewport()); + p.setWorldTransform(QTransform().scale(d->zoomFactor, d->zoomFactor)); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + p.setRenderHint(QPainter::Antialiasing, true); + d->documentContainer.draw(&p, toVirtual(event->rect())); +} + +void QLiteHtmlWidget::resizeEvent(QResizeEvent *event) +{ + withFixedTextPosition([this, 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(fromVirtual(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(fromVirtual(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(fromVirtual(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(fromVirtual(r.translated(-scrollPosition()))); + } +} + +void QLiteHtmlWidget::leaveEvent(QEvent *event) +{ + Q_UNUSED(event) + for (const QRect &r : d->documentContainer.leaveEvent()) + viewport()->update(fromVirtual(r.translated(-scrollPosition()))); +} + +void QLiteHtmlWidget::contextMenuEvent(QContextMenuEvent *event) +{ + QPoint viewportPos; + QPoint pos; + htmlPos(event->pos(), &viewportPos, &pos); + emit contextMenuRequested(event->pos(), d->documentContainer.linkAt(pos, viewportPos)); +} + +void QLiteHtmlWidget::withFixedTextPosition(const std::function<void()> &action) +{ + // remember element to which to scroll after re-rendering + QPoint viewportPos; + QPoint pos; + htmlPos({}, &viewportPos, &pos); // top-left + const int y = d->documentContainer.withFixedElementPosition(pos.y(), action); + if (y >= 0) + verticalScrollBar()->setValue(std::min(y, verticalScrollBar()->maximum())); +} + +void QLiteHtmlWidget::render() +{ + if (!d->documentContainer.hasDocument()) + return; + const int fullWidth = width() / d->zoomFactor; + const QSize vViewportSize = toVirtual(viewport()->size()); + const int scrollbarWidth = style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, this); + const int w = fullWidth - scrollbarWidth - 2; + d->documentContainer.render(w, vViewportSize.height()); + // scroll bars reflect virtual/scaled size of html document + horizontalScrollBar()->setPageStep(vViewportSize.width()); + horizontalScrollBar()->setRange(0, std::max(0, d->documentContainer.documentWidth() - w)); + verticalScrollBar()->setPageStep(vViewportSize.height()); + verticalScrollBar() + ->setRange(0, std::max(0, d->documentContainer.documentHeight() - vViewportSize.height())); + viewport()->update(); +} + +QPoint QLiteHtmlWidget::scrollPosition() const +{ + return {horizontalScrollBar()->value(), verticalScrollBar()->value()}; +} + +void QLiteHtmlWidget::htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const +{ + *viewportPos = toVirtual(viewport()->mapFromParent(pos)); + *htmlPos = *viewportPos + scrollPosition(); +} + +QPoint QLiteHtmlWidget::toVirtual(const QPoint &p) const +{ + return {int(p.x() / d->zoomFactor), int(p.y() / d->zoomFactor)}; +} + +QSize QLiteHtmlWidget::toVirtual(const QSize &s) const +{ + return {int(s.width() / d->zoomFactor), int(s.height() / d->zoomFactor)}; +} + +QRect QLiteHtmlWidget::toVirtual(const QRect &r) const +{ + return {toVirtual(r.topLeft()), toVirtual(r.size())}; +} + +QRect QLiteHtmlWidget::fromVirtual(const QRect &r) const +{ + const QPoint tl{int(r.x() * d->zoomFactor), int(r.y() * d->zoomFactor)}; + // round size up, and add one since the topleft point was rounded down + const QSize s{int(r.width() * d->zoomFactor + 0.5) + 1, + int(r.height() * d->zoomFactor + 0.5) + 1}; + return {tl, s}; +} diff --git a/src/qlitehtmlwidget.h b/src/qlitehtmlwidget.h new file mode 100644 index 0000000..3722dce --- /dev/null +++ b/src/qlitehtmlwidget.h @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** 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 +** 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 "qlitehtml_global.h" + +#include <QAbstractScrollArea> +#include <QTextDocument> + +#include <functional> + +class QLiteHtmlWidgetPrivate; + +class QLITEHTML_EXPORT QLiteHtmlWidget : public QAbstractScrollArea +{ + Q_OBJECT +public: + explicit QLiteHtmlWidget(QWidget *parent = nullptr); + ~QLiteHtmlWidget() override; + + // declaring the getters Q_INVOKABLE to make them Squish-testable + void setUrl(const QUrl &url); + Q_INVOKABLE QUrl url() const; + void setHtml(const QString &content); + Q_INVOKABLE QString html() const; + Q_INVOKABLE QString title() const; + + void setZoomFactor(qreal scale); + qreal zoomFactor() const; + + bool findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped = nullptr); + + void setDefaultFont(const QFont &font); + QFont defaultFont() const; + + void scrollToAnchor(const QString &name); + + using ResourceHandler = std::function<QByteArray(QUrl)>; + void setResourceHandler(const ResourceHandler &handler); + + // declaring this Q_INVOKABLE to make it Squish-testable + Q_INVOKABLE QString selectedText() const; + +signals: + void linkClicked(const QUrl &url); + void contextMenuRequested(const QPoint &pos, 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; + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + void withFixedTextPosition(const std::function<void()> &action); + void render(); + QPoint scrollPosition() const; + void htmlPos(const QPoint &pos, QPoint *viewportPos, QPoint *htmlPos) const; + QPoint toVirtual(const QPoint &p) const; + QSize toVirtual(const QSize &s) const; + QRect toVirtual(const QRect &r) const; + QRect fromVirtual(const QRect &r) const; + + QLiteHtmlWidgetPrivate *d; +}; |