diff --git a/src/plugins/help/litehtmlhelpviewer.cpp b/src/plugins/help/litehtmlhelpviewer.cpp index 8dc360f1ccf..1738280d216 100644 --- a/src/plugins/help/litehtmlhelpviewer.cpp +++ b/src/plugins/help/litehtmlhelpviewer.cpp @@ -198,8 +198,10 @@ void LiteHtmlHelpViewer::addForwardHistoryItems(QMenu *forwardMenu) bool LiteHtmlHelpViewer::findText( const QString &text, Core::FindFlags flags, bool incremental, bool fromSearch, bool *wrapped) { - // TODO - return false; + return m_viewer->findText(text, + Core::textDocumentFlagsForFindFlags(flags), + incremental, + wrapped); } void LiteHtmlHelpViewer::copy() diff --git a/src/plugins/help/qlitehtml/container_qpainter.cpp b/src/plugins/help/qlitehtml/container_qpainter.cpp index 3426d098c29..edd88ef0cef 100644 --- a/src/plugins/help/qlitehtml/container_qpainter.cpp +++ b/src/plugins/help/qlitehtml/container_qpainter.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -134,6 +135,22 @@ static int findChild(const litehtml::element::ptr &child, const litehtml::elemen 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) { @@ -152,10 +169,7 @@ static litehtml::element::ptr nextLeaf(const litehtml::element::ptr &element, 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; + return firstLeaf(current, stop); } static Selection::Element selectionDetails(const litehtml::element::ptr &element, @@ -609,6 +623,29 @@ void DocumentContainer::drawSelection(QPainter *painter, const QRect &clip) cons painter->restore(); } +void DocumentContainer::buildIndex() +{ + m_index.elementToIndex.clear(); + m_index.indexToElement.clear(); + m_index.text.clear(); + + int index = 0; + litehtml::element::ptr current = firstLeaf(m_document->root(), nullptr); + while (current != m_document->root()) { + m_index.elementToIndex.insert({current, index}); + if (current->is_visible()) { + litehtml::tstring text; + current->get_text(text); + if (!text.empty()) { + m_index.indexToElement.push_back({index, current}); + m_index.text += QString::fromStdString(text); + index += text.size(); + } + } + current = nextLeaf(current, m_document->root()); + } +} + void DocumentContainer::draw_background(litehtml::uint_ptr hdc, const litehtml::background_paint &bg) { // TODO @@ -877,6 +914,7 @@ void DocumentContainer::setDocument(const QByteArray &data, litehtml::context *c m_pixmaps.clear(); m_selection = {}; m_document = litehtml::document::createFromUTF8(data.constData(), this, context); + buildIndex(); } litehtml::document::ptr DocumentContainer::document() const @@ -1051,6 +1089,100 @@ QString DocumentContainer::selectedText() const return m_selection.text; } +void DocumentContainer::findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped, + bool *success, + QVector *oldSelection, + QVector *newSelection) +{ + if (success) + *success = false; + if (oldSelection) + oldSelection->clear(); + if (newSelection) + newSelection->clear(); + if (!m_document) + return; + const bool backward = flags & QTextDocument::FindBackward; + int startIndex = backward ? -1 : 0; + if (m_selection.startElem.element && 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(m_selection.startElem, 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 = m_index.elementToIndex.find(searchStart.element); + if (findInIndex == std::end(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 = 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 ? m_index.text.lastIndexOf(expression, startIndex) + : m_index.text.indexOf(expression, startIndex); + if (foundIndex < 0) { // wrap + foundIndex = backward ? m_index.text.lastIndexOf(expression) + : m_index.text.indexOf(expression); + if (wrapped && foundIndex >= 0) + *wrapped = true; + } + if (foundIndex >= 0) { + const Index::Entry startEntry = m_index.findElement(foundIndex); + const Index::Entry endEntry = m_index.findElement(foundIndex + text.size()); + if (!startEntry.second || !endEntry.second) { + qWarning() << "internal error: search ended up with nullptr elements"; + return; + } + if (oldSelection) + *oldSelection = m_selection.selection; + m_selection = {}; + m_selection.startElem = fillXPos({startEntry.second, foundIndex - startEntry.first, -1}); + m_selection.endElem = fillXPos( + {endEntry.second, foundIndex + text.size() - endEntry.first, -1}); + m_selection.update(); + if (newSelection) + *newSelection = m_selection.selection; + if (success) + *success = true; + return; + } + return; +} + void DocumentContainer::setDefaultFont(const QFont &font) { m_defaultFont = font; @@ -1122,3 +1254,16 @@ QUrl DocumentContainer::resolveUrl(const QString &url, const QString &baseUrl) c } 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); +} diff --git a/src/plugins/help/qlitehtml/container_qpainter.h b/src/plugins/help/qlitehtml/container_qpainter.h index 0d2c0425e02..7c665c410d5 100644 --- a/src/plugins/help/qlitehtml/container_qpainter.h +++ b/src/plugins/help/qlitehtml/container_qpainter.h @@ -33,9 +33,11 @@ #include #include #include +#include #include #include +#include class Selection { @@ -64,12 +66,25 @@ public: bool isSelecting = false; }; +struct Index +{ + QString text; + // only contains leaf elements + std::unordered_map elementToIndex; + + using Entry = std::pair; + std::vector indexToElement; + + Entry findElement(int index) const; +}; + class DocumentContainer : public litehtml::document_container { public: DocumentContainer(); virtual ~DocumentContainer(); +public: // document_container API litehtml::uint_ptr create_font(const litehtml::tchar_t *faceName, int size, int weight, @@ -121,6 +136,7 @@ public: void get_media_features(litehtml::media_features &media) const override; void get_language(litehtml::tstring &language, litehtml::tstring &culture) const override; +public: // outside API void setPaintDevice(QPaintDevice *paintDevice); void setDocument(const QByteArray &data, litehtml::context *context); litehtml::document::ptr document() const; @@ -145,6 +161,14 @@ public: QString caption() const; QString selectedText() const; + void findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped, + bool *success, + QVector *oldSelection, + QVector *newSelection); + void setDefaultFont(const QFont &font); QFont defaultFont() const; @@ -167,9 +191,11 @@ private: 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; diff --git a/src/plugins/help/qlitehtml/qlitehtmlwidget.cpp b/src/plugins/help/qlitehtml/qlitehtmlwidget.cpp index bcb63567592..66c5a5f2bbb 100644 --- a/src/plugins/help/qlitehtml/qlitehtmlwidget.cpp +++ b/src/plugins/help/qlitehtml/qlitehtmlwidget.cpp @@ -434,6 +434,34 @@ QString QLiteHtmlWidget::title() const return d->documentContainer.caption(); } +bool QLiteHtmlWidget::findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped) +{ + bool success = false; + QVector oldSelection; + QVector 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); + if (success && verticalScrollBar()->value() > newSelectionCombined.top()) { + verticalScrollBar()->setValue(newSelectionCombined.top()); + } else if (success + && verticalScrollBar()->value() + viewport()->height() + < newSelectionCombined.bottom()) { + verticalScrollBar()->setValue(newSelectionCombined.bottom() - viewport()->height()); + } else { + viewport()->update(newSelectionCombined.translated(-scrollPosition())); + for (const QRect &r : oldSelection) + viewport()->update(r.translated(-scrollPosition())); + } + return success; +} + void QLiteHtmlWidget::setDefaultFont(const QFont &font) { d->documentContainer.setDefaultFont(font); diff --git a/src/plugins/help/qlitehtml/qlitehtmlwidget.h b/src/plugins/help/qlitehtml/qlitehtmlwidget.h index b2f1df6cf4c..54c3d65baa5 100644 --- a/src/plugins/help/qlitehtml/qlitehtmlwidget.h +++ b/src/plugins/help/qlitehtml/qlitehtmlwidget.h @@ -26,6 +26,7 @@ #pragma once #include +#include #include @@ -43,6 +44,11 @@ public: void setHtml(const QString &content); QString title() const; + bool findText(const QString &text, + QTextDocument::FindFlags flags, + bool incremental, + bool *wrapped = nullptr); + void setDefaultFont(const QFont &font); QFont defaultFont() const;