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;