forked from qt-creator/qt-creator
Help/litehtml: Implement text search
Build an index between (visible) text and litehtml elements, so we can use the regular text search functionality of Qt. Change-Id: I2bf29628482f81c67b20b9a02d7808a330a073eb Reviewed-by: Cristian Adam <cristian.adam@qt.io> Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
@@ -198,8 +198,10 @@ void LiteHtmlHelpViewer::addForwardHistoryItems(QMenu *forwardMenu)
|
|||||||
bool LiteHtmlHelpViewer::findText(
|
bool LiteHtmlHelpViewer::findText(
|
||||||
const QString &text, Core::FindFlags flags, bool incremental, bool fromSearch, bool *wrapped)
|
const QString &text, Core::FindFlags flags, bool incremental, bool fromSearch, bool *wrapped)
|
||||||
{
|
{
|
||||||
// TODO
|
return m_viewer->findText(text,
|
||||||
return false;
|
Core::textDocumentFlagsForFindFlags(flags),
|
||||||
|
incremental,
|
||||||
|
wrapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LiteHtmlHelpViewer::copy()
|
void LiteHtmlHelpViewer::copy()
|
||||||
|
@@ -35,6 +35,7 @@
|
|||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QTextLayout>
|
#include <QTextLayout>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
@@ -134,6 +135,22 @@ static int findChild(const litehtml::element::ptr &child, const litehtml::elemen
|
|||||||
return -1;
|
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,
|
static litehtml::element::ptr nextLeaf(const litehtml::element::ptr &element,
|
||||||
const litehtml::element::ptr &stop)
|
const litehtml::element::ptr &stop)
|
||||||
{
|
{
|
||||||
@@ -152,10 +169,7 @@ static litehtml::element::ptr nextLeaf(const litehtml::element::ptr &element,
|
|||||||
return nextLeaf(parent, stop);
|
return nextLeaf(parent, stop);
|
||||||
current = parent->get_child(childIndex + 1);
|
current = parent->get_child(childIndex + 1);
|
||||||
}
|
}
|
||||||
// move down to first leaf
|
return firstLeaf(current, stop);
|
||||||
while (current != stop && current->get_children_count() > 0)
|
|
||||||
current = current->get_child(0);
|
|
||||||
return current;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Selection::Element selectionDetails(const litehtml::element::ptr &element,
|
static Selection::Element selectionDetails(const litehtml::element::ptr &element,
|
||||||
@@ -609,6 +623,29 @@ void DocumentContainer::drawSelection(QPainter *painter, const QRect &clip) cons
|
|||||||
painter->restore();
|
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)
|
void DocumentContainer::draw_background(litehtml::uint_ptr hdc, const litehtml::background_paint &bg)
|
||||||
{
|
{
|
||||||
// TODO
|
// TODO
|
||||||
@@ -877,6 +914,7 @@ void DocumentContainer::setDocument(const QByteArray &data, litehtml::context *c
|
|||||||
m_pixmaps.clear();
|
m_pixmaps.clear();
|
||||||
m_selection = {};
|
m_selection = {};
|
||||||
m_document = litehtml::document::createFromUTF8(data.constData(), this, context);
|
m_document = litehtml::document::createFromUTF8(data.constData(), this, context);
|
||||||
|
buildIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
litehtml::document::ptr DocumentContainer::document() const
|
litehtml::document::ptr DocumentContainer::document() const
|
||||||
@@ -1051,6 +1089,100 @@ QString DocumentContainer::selectedText() const
|
|||||||
return m_selection.text;
|
return 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 (!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)
|
void DocumentContainer::setDefaultFont(const QFont &font)
|
||||||
{
|
{
|
||||||
m_defaultFont = font;
|
m_defaultFont = font;
|
||||||
@@ -1122,3 +1254,16 @@ QUrl DocumentContainer::resolveUrl(const QString &url, const QString &baseUrl) c
|
|||||||
}
|
}
|
||||||
return qurl;
|
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);
|
||||||
|
}
|
||||||
|
@@ -33,9 +33,11 @@
|
|||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QTextDocument>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
class Selection
|
class Selection
|
||||||
{
|
{
|
||||||
@@ -64,12 +66,25 @@ public:
|
|||||||
bool isSelecting = false;
|
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 DocumentContainer : public litehtml::document_container
|
class DocumentContainer : public litehtml::document_container
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DocumentContainer();
|
DocumentContainer();
|
||||||
virtual ~DocumentContainer();
|
virtual ~DocumentContainer();
|
||||||
|
|
||||||
|
public: // document_container API
|
||||||
litehtml::uint_ptr create_font(const litehtml::tchar_t *faceName,
|
litehtml::uint_ptr create_font(const litehtml::tchar_t *faceName,
|
||||||
int size,
|
int size,
|
||||||
int weight,
|
int weight,
|
||||||
@@ -121,6 +136,7 @@ public:
|
|||||||
void get_media_features(litehtml::media_features &media) const override;
|
void get_media_features(litehtml::media_features &media) const override;
|
||||||
void get_language(litehtml::tstring &language, litehtml::tstring &culture) const override;
|
void get_language(litehtml::tstring &language, litehtml::tstring &culture) const override;
|
||||||
|
|
||||||
|
public: // outside API
|
||||||
void setPaintDevice(QPaintDevice *paintDevice);
|
void setPaintDevice(QPaintDevice *paintDevice);
|
||||||
void setDocument(const QByteArray &data, litehtml::context *context);
|
void setDocument(const QByteArray &data, litehtml::context *context);
|
||||||
litehtml::document::ptr document() const;
|
litehtml::document::ptr document() const;
|
||||||
@@ -145,6 +161,14 @@ public:
|
|||||||
QString caption() const;
|
QString caption() const;
|
||||||
QString selectedText() 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);
|
void setDefaultFont(const QFont &font);
|
||||||
QFont defaultFont() const;
|
QFont defaultFont() const;
|
||||||
|
|
||||||
@@ -167,9 +191,11 @@ private:
|
|||||||
QString monospaceFont() const;
|
QString monospaceFont() const;
|
||||||
QUrl resolveUrl(const QString &url, const QString &baseUrl) const;
|
QUrl resolveUrl(const QString &url, const QString &baseUrl) const;
|
||||||
void drawSelection(QPainter *painter, const QRect &clip) const;
|
void drawSelection(QPainter *painter, const QRect &clip) const;
|
||||||
|
void buildIndex();
|
||||||
|
|
||||||
QPaintDevice *m_paintDevice = nullptr;
|
QPaintDevice *m_paintDevice = nullptr;
|
||||||
litehtml::document::ptr m_document;
|
litehtml::document::ptr m_document;
|
||||||
|
Index m_index;
|
||||||
QString m_baseUrl;
|
QString m_baseUrl;
|
||||||
QRect m_clientRect;
|
QRect m_clientRect;
|
||||||
QPoint m_scrollPosition;
|
QPoint m_scrollPosition;
|
||||||
|
@@ -434,6 +434,34 @@ QString QLiteHtmlWidget::title() const
|
|||||||
return d->documentContainer.caption();
|
return d->documentContainer.caption();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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)
|
void QLiteHtmlWidget::setDefaultFont(const QFont &font)
|
||||||
{
|
{
|
||||||
d->documentContainer.setDefaultFont(font);
|
d->documentContainer.setDefaultFont(font);
|
||||||
|
@@ -26,6 +26,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractScrollArea>
|
#include <QAbstractScrollArea>
|
||||||
|
#include <QTextDocument>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
@@ -43,6 +44,11 @@ public:
|
|||||||
void setHtml(const QString &content);
|
void setHtml(const QString &content);
|
||||||
QString title() const;
|
QString title() const;
|
||||||
|
|
||||||
|
bool findText(const QString &text,
|
||||||
|
QTextDocument::FindFlags flags,
|
||||||
|
bool incremental,
|
||||||
|
bool *wrapped = nullptr);
|
||||||
|
|
||||||
void setDefaultFont(const QFont &font);
|
void setDefaultFont(const QFont &font);
|
||||||
QFont defaultFont() const;
|
QFont defaultFont() const;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user