From c621713b8cbffae7874064c1cce0e6a3e8405eb2 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 29 Oct 2025 05:45:05 +0100 Subject: [PATCH 01/15] initial impl for selection --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 3 + src/CMakeLists.txt | 1 + src/QLiteHtmlBrowserImpl.cpp | 1 + src/QLiteHtmlBrowserImpl.h | 1 + src/TextHelpers.h | 305 ++++++++++++++++++++ src/container_qt.cpp | 198 +++++++++++-- src/container_qt.h | 118 ++++---- test/browser/testbrowser.cpp | 8 + test/browser/testbrowser.h | 2 + 9 files changed, 557 insertions(+), 80 deletions(-) create mode 100644 src/TextHelpers.h diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 200766b..0c0ca5b 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -188,6 +188,9 @@ public Q_SLOTS: /// send when scale changes void scaleChanged(); + /// send when selection changes + void selectionChanged( const QString& selectedText ); + protected: private: QLiteHtmlBrowserImpl* mImpl = nullptr; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 27f34fd..64188db 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources(QLiteHtmlBrowser QLiteHtmlBrowser.cpp QLiteHtmlBrowserImpl.cpp QLiteHtmlBrowserImpl.h + TextHelpers.h container_qt.cpp container_qt.h browserdefinitions.h diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index 9f000de..55b11b8 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -25,6 +25,7 @@ QLiteHtmlBrowserImpl::QLiteHtmlBrowserImpl( QWidget* parent ) shortcut = new QShortcut( QKeySequence{ QKeySequence::Back }, this ); connect( shortcut, &QShortcut::activated, this, &QLiteHtmlBrowserImpl::backward ); connect( mContainer, &container_qt::scaleChanged, this, &QLiteHtmlBrowserImpl::scaleChanged ); + connect( mContainer, &container_qt::selectionChanged, this, &QLiteHtmlBrowserImpl::selectionChanged ); auto* layout = new QVBoxLayout; layout->setContentsMargins( 0, 0, 0, 0 ); diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 17ad6e0..dd502f4 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -79,6 +79,7 @@ class QLiteHtmlBrowserImpl : public QWidget void urlChanged( const QUrl& ); void anchorClicked( const QUrl& ); void scaleChanged(); + void selectionChanged( const QString& selectedText ); private: class HistoryEntry diff --git a/src/TextHelpers.h b/src/TextHelpers.h new file mode 100644 index 0000000..5fa95a0 --- /dev/null +++ b/src/TextHelpers.h @@ -0,0 +1,305 @@ +#pragma once + +#include +#include + +namespace TextHelpers +{ + +struct TextFragment +{ + std::string text; + litehtml::element::ptr element; + litehtml::position pos; + int start_char_offset; // Offset in element text + int end_char_offset; // end Offset in element text + int global_start_offset; + int global_end_offset; + bool is_leaf; + + bool hasValidPosition() const { return pos.width > 0 && pos.height > 0; } +}; + +struct TextFindMatch +{ + std::string matched_text; + std::vector fragments; // may contains multiple elements + litehtml::position bounding_box; // bounding box over all elements +}; + +struct TextPosition +{ + litehtml::element::ptr element; + int char_offset; // Zeichenposition im Element-Text + litehtml::position element_pos; + + bool operator<( const TextPosition& other ) const + { + // Vergleich basierend auf Position im Dokument + if ( element_pos.y != other.element_pos.y ) + { + return element_pos.y < other.element_pos.y; + } + return element_pos.x < other.element_pos.x; + } + + bool isValid() const { return element != nullptr; } +}; + +struct SelectionRange +{ + TextPosition start; + TextPosition end; + std::vector fragments; // Alle Textfragmente in der Selektion + + bool isEmpty() const { return !start.isValid() || !end.isValid(); } + + void clear() + { + start = TextPosition(); + end = TextPosition(); + fragments.clear(); + } +}; + +class DOMTextManager +{ +private: + std::vector m_fragments; + std::string m_fullText; + +public: + void buildFromDocument( litehtml::document::ptr doc ) + { + m_fragments.clear(); + m_fullText.clear(); + + if ( doc && doc->root() ) + { + collectTextFragments( doc->root() ); + + qDebug() << "\n=== Gesammelte Text-Fragmente ==="; + qDebug() << "Anzahl Fragmente:" << m_fragments.size(); + for ( size_t i = 0; i < m_fragments.size(); ++i ) + { + const auto& frag = m_fragments[i]; + qDebug() << "Fragment" << i << ":" + << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y + << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); + } + qDebug() << "================================\n"; + } + } + + // Findet die Text-Position basierend auf Pixel-Koordinaten + TextPosition getPositionAtCoordinates( int x, int y ) + { + TextPosition result; + TextFragment* bestMatch = nullptr; + int smallestArea = INT_MAX; + + qDebug() << "\n>>> Suche Element bei:" << x << y; + + // Suche das kleinste Element, das den Punkt enthält (spezifischstes Element) + for ( auto& fragment : m_fragments ) + { + // Nur Leaf-Nodes und Elemente mit gültiger Position prüfen + if ( !fragment.is_leaf || !fragment.hasValidPosition() ) + { + continue; + } + + const auto& pos = fragment.pos; + + // Prüfe ob Punkt im Element liegt + if ( x >= pos.x && x <= pos.x + pos.width && y >= pos.y && y <= pos.y + pos.height ) + { + + int area = pos.width * pos.height; + + qDebug() << " Kandidat:" << QString::fromStdString( fragment.text ).left( 20 ) << "| Box:" << pos.x << pos.y << pos.width << pos.height + << "| Area:" << area; + + // Wähle das kleinste umschließende Element + if ( area < smallestArea ) + { + smallestArea = area; + bestMatch = &fragment; + } + } + } + + if ( bestMatch ) + { + result.element = bestMatch->element; + result.element_pos = bestMatch->pos; + + // Berechne Zeichen-Offset basierend auf x-Position + const auto& pos = bestMatch->pos; + if ( pos.width > 0 && bestMatch->text.length() > 0 ) + { + float relativeX = static_cast( x - pos.x ) / pos.width; + result.char_offset = static_cast( relativeX * bestMatch->text.length() ); + result.char_offset = std::max( 0, std::min( result.char_offset, static_cast( bestMatch->text.length() ) ) ); + } + else + { + result.char_offset = 0; + } + + qDebug() << ">>> TREFFER:" << QString::fromStdString( bestMatch->text ).left( 30 ) << "| Offset:" << result.char_offset; + } + else + { + qDebug() << ">>> KEIN TREFFER!"; + } + + return result; + } + + // Extrahiert Text und Fragmente zwischen zwei Positionen + SelectionRange getSelectionBetween( const TextPosition& start, const TextPosition& end ) + { + SelectionRange selection; + selection.start = start; + selection.end = end; + + if ( !start.isValid() || !end.isValid() ) + { + return selection; + } + + // Sicherstellen dass start vor end liegt + TextPosition actualStart = start; + TextPosition actualEnd = end; + if ( end < start ) + { + std::swap( actualStart, actualEnd ); + } + + bool inSelection = false; + + // Nur durch Leaf-Nodes iterieren + for ( const auto& fragment : m_fragments ) + { + if ( !fragment.is_leaf || !fragment.hasValidPosition() ) + { + continue; + } + + bool isStartFragment = ( fragment.element == actualStart.element ); + bool isEndFragment = ( fragment.element == actualEnd.element ); + + if ( isStartFragment ) + { + inSelection = true; + } + + if ( inSelection ) + { + TextFragment selectedFragment = fragment; + int startOffset = 0; + int endOffset = fragment.text.length(); + + if ( isStartFragment ) + { + startOffset = actualStart.char_offset; + } + + if ( isEndFragment ) + { + endOffset = actualEnd.char_offset; + } + + selectedFragment.start_char_offset = startOffset; + selectedFragment.end_char_offset = endOffset; + + selection.fragments.push_back( selectedFragment ); + + if ( isEndFragment ) + { + break; + } + } + } + + qDebug() << "Selektion umfasst" << selection.fragments.size() << "Fragmente"; + + return selection; + } + + const std::vector& getFragments() const { return m_fragments; } +private: + std::string normalizeWhitespace( const std::string& text ) + { + std::string result; + bool lastWasSpace = false; + + for ( char c : text ) + { + if ( std::isspace( static_cast( c ) ) ) + { + if ( !lastWasSpace && !result.empty() ) + { + result += ' '; + lastWasSpace = true; + } + } + else + { + result += c; + lastWasSpace = false; + } + } + + return result; + } + + void collectTextFragments( litehtml::element::ptr el ) + { + if (!el) return; + + // Prüfe ob Element Kinder hat + bool hasChildren = !el->children().empty(); + + // Text aus dem Element holen + std::string text; + el->get_text( text ); + + // NUR Leaf-Nodes (Elemente ohne Kinder) mit Text speichern + // Dadurch vermeiden wir, dass Parent-Elemente den gesamten + // aggregierten Text ihrer Kinder zurückgeben + if ( !hasChildren && !text.empty() ) + { + litehtml::position pos = el->get_placement(); + + // Nur wenn Element eine gültige Position hat + if ( pos.width > 0 && pos.height > 0 ) + { + TextFragment fragment; + fragment.text = normalizeWhitespace( text ); + fragment.element = el; + fragment.pos = pos; + fragment.start_char_offset = 0; + fragment.end_char_offset = fragment.text.length(); + fragment.global_start_offset = m_fullText.length(); + fragment.is_leaf = true; + + m_fullText += fragment.text + " "; + fragment.global_end_offset = m_fullText.length(); + + m_fragments.push_back( fragment ); + + qDebug() << "Leaf-Fragment:" << QString::fromStdString( fragment.text ).left( 30 ) << "| Pos:" << pos.x << pos.y << "| Size:" << pos.width + << "x" << pos.height; + } + } + + // Rekursiv durch alle Kindelemente + for ( auto it = el->children().begin(); it != el->children().end(); ++it ) + { + collectTextFragments( *it ); + } + } +}; +} diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 18c8f85..3523835 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -16,11 +16,14 @@ #include #include #include +#include #include #include #include +using namespace TextHelpers; + static const auto CSS_GENERIC_FONT_TO_QFONT_STYLEHINT = QMap{ { "serif", QFont::StyleHint::Serif }, { "sans-serif", QFont::StyleHint::SansSerif }, { "monospace", QFont::StyleHint::Monospace }, @@ -46,6 +49,7 @@ container_qt::container_qt( QWidget* parent ) connect( shortcut, &QShortcut::activated, this, [this]() { setScale( 1.0 ); } ); setMouseTracking( true ); + setFocusPolicy( Qt::StrongFocus ); } void container_qt::setCSS( const QString& master_css, const QString& user_css ) @@ -72,7 +76,10 @@ void container_qt::setHtml( const QString& html, const QUrl& source_url ) mDocumentSource, this, mMasterCSS.isEmpty() ? litehtml::master_css : mMasterCSS.toUtf8().constData(), mUserCSS.toUtf8().constData() ); verticalScrollBar()->setValue( 0 ); horizontalScrollBar()->setValue( 0 ); + + m_currentSelection.clear(); render(); + m_textManager.buildFromDocument( mDocument ); auto frag = source_url.fragment(); if ( !frag.isEmpty() ) @@ -127,6 +134,10 @@ void container_qt::paintEvent( QPaintEvent* event ) mDocument->draw( hdc, margins.left() + scroll_pos.x(), margins.top() + scroll_pos.y(), &clipRect ); draw_highlights( hdc ); + if ( !m_currentSelection.isEmpty() ) + { + drawSelection( p ); + } } } } @@ -979,7 +990,6 @@ QRect container_qt::inv_scaled( const QRect& rect ) const void container_qt::mouseMoveEvent( QMouseEvent* e ) { - if ( e && mDocument ) { @@ -989,6 +999,17 @@ void container_qt::mouseMoveEvent( QMouseEvent* e ) { // something changed, redraw of boxes required; } + if ( m_isSelecting && m_selectionStart.isValid() ) + { + TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.client.x(), mousePos.client.y() ); + + if ( currentPos.isValid() ) + { + m_currentSelection = m_textManager.getSelectionBetween( m_selectionStart, currentPos ); + update(); + } + } + // bool litehtml::document::on_mouse_leave( position::vector& redraw_boxes ); } } @@ -1005,8 +1026,23 @@ void container_qt::mouseReleaseEvent( QMouseEvent* e ) // something changed, redraw of boxes required; e->accept(); } - else - e->ignore(); + if ( e->button() == Qt::LeftButton ) + { + qDebug() << "selection finished"; + m_isSelecting = false; + + if ( !m_currentSelection.isEmpty() ) + { + qDebug() << "selection got selected text with " << getSelectedText().count() << "fragments"; + qDebug() << "\n=== Selektierter Text ==="; + qDebug() << getSelectedText(); + qDebug() << "================================\n"; + emit selectionChanged( getSelectedText() ); + copySelectionToClipboard(); + } + } + // else + // e->ignore(); } else e->ignore(); @@ -1023,15 +1059,14 @@ void container_qt::mousePressEvent( QMouseEvent* e ) if ( mDocument->on_lbutton_down( mousePos.html.x(), mousePos.html.y(), mousePos.client.x(), mousePos.client.y(), redraw_boxes ) ) { // something changed, redraw of boxes required; - e->accept(); } - // else - // { - // auto elem = mDocument->get_over_element(); - // qDebug() << elem->get_placement().x << elem->get_tagName(); - // e->ignore(); - // } + m_isSelecting = true; + m_selectionStart = m_textManager.getPositionAtCoordinates( mousePos.client.x(), mousePos.client.y() ); + qDebug() << "selection started with" << QPoint( m_selectionStart.element_pos.x, m_selectionStart.element_pos.y ); + m_currentSelection.clear(); + e->accept(); + update(); } else e->ignore(); @@ -1131,15 +1166,15 @@ void container_qt::collect_text_fragments( litehtml::element::ptr el, std::vecto if ( !text.empty() && el->is_text() ) { TextFragment fragment; - fragment.text = text; - fragment.element = el; - fragment.pos = el->get_placement(); - fragment.start_offset = static_cast( fullText.length() ); + fragment.text = text; + fragment.element = el; + fragment.pos = el->get_placement(); + fragment.start_char_offset = static_cast( fullText.length() ); std::string normalized = normalizeWhitespace( text ); fullText += normalized; - fragment.end_offset = static_cast( fullText.length() ); + fragment.end_char_offset = static_cast( fullText.length() ); fragments.push_back( fragment ); // Leerzeichen zwischen Elementen @@ -1167,8 +1202,8 @@ litehtml::position container_qt::calculate_precise_bounding_box( const std::vect for ( const auto& fragment : allFragments ) { // check if this fragment is part of the search - int overlapStart = std::max( searchStart, fragment.start_offset ); - int overlapEnd = std::min( searchEnd, fragment.end_offset ); + int overlapStart = std::max( searchStart, fragment.start_char_offset ); + int overlapEnd = std::min( searchEnd, fragment.end_char_offset ); if ( overlapStart < overlapEnd ) { @@ -1176,11 +1211,11 @@ litehtml::position container_qt::calculate_precise_bounding_box( const std::vect TextFragment matchedFragment = fragment; // offsets relative to start - int fragmentTextStart = overlapStart - fragment.start_offset; - int fragmentTextEnd = overlapEnd - fragment.start_offset; + int fragmentTextStart = overlapStart - fragment.start_char_offset; + int fragmentTextEnd = overlapEnd - fragment.start_char_offset; - matchedFragment.start_offset = fragmentTextStart; - matchedFragment.end_offset = fragmentTextEnd; + matchedFragment.start_char_offset = fragmentTextStart; + matchedFragment.end_char_offset = fragmentTextEnd; // calculate width of text // Importnat: use text_width from document_container @@ -1328,7 +1363,7 @@ bool container_qt::find_previous_match() } // Aktuelles Suchergebnis mit Position holen -const container_qt::TextFindMatch* container_qt::find_current_match() const +const TextHelpers::TextFindMatch* container_qt::find_current_match() const { if ( mFindCurrentMatchIndex >= 0 && mFindCurrentMatchIndex < static_cast( mFindMatches.size() ) ) { @@ -1398,3 +1433,122 @@ void container_qt::scroll_to_find_match( const TextFindMatch* match ) horizontalScrollBar()->setValue( match->bounding_box.left() ); verticalScrollBar()->setValue( match->bounding_box.top() ); } + +void container_qt::copySelectionToClipboard() +{ + if ( m_currentSelection.isEmpty() ) + { + return; + } + + QString selectedText = getSelectedText(); + if ( !selectedText.isEmpty() ) + { + QClipboard* clipboard = QApplication::clipboard(); + clipboard->setText( selectedText ); + } +} + +// Gibt selektierten Text zurück +QString container_qt::getSelectedText() const +{ + if ( m_currentSelection.isEmpty() ) + { + return QString(); + } + + QString result; + // for ( const auto& fragment : m_currentSelection.fragments ) + // { + // QString fragmentText = + // QString::fromStdString( fragment.text.substr( fragment.start_char_offset, fragment.end_char_offset - fragment.start_char_offset ) ); + // result += fragmentText; + // } + + for ( size_t i = 0; i < m_currentSelection.fragments.size(); ++i ) + { + const auto& fragment = m_currentSelection.fragments[i]; + + if ( fragment.end_char_offset > fragment.start_char_offset ) + { + QString fragmentText = + QString::fromStdString( fragment.text.substr( fragment.start_char_offset, fragment.end_char_offset - fragment.start_char_offset ) ); + result += fragmentText; + + // Leerzeichen zwischen Fragmenten hinzufügen (außer beim letzten) + if ( i < m_currentSelection.fragments.size() - 1 ) + { + result += " "; + } + } + } + + return result; + + return result; +} + +const SelectionRange& container_qt::getCurrentSelection() const +{ + return m_currentSelection; +} + +void container_qt::clearSelection() +{ + m_currentSelection.clear(); + update(); +} + +void container_qt::keyPressEvent( QKeyEvent* event ) +{ + // Strg+C zum Kopieren + if ( event->matches( QKeySequence::Copy ) ) + { + copySelectionToClipboard(); + } + // Escape zum Abbrechen der Selektion + else if ( event->key() == Qt::Key_Escape ) + { + clearSelection(); + } + + QWidget::keyPressEvent( event ); +} + +void container_qt::drawSelection( QPainter& painter ) +{ + qDebug() << "container_qt::drawSelection"; + // Farben für Selektion (Windows-Style) + QColor selectionColor( 0, 120, 215, 100 ); // Blau mit Transparenz + QColor borderColor( 0, 120, 215, 180 ); + + painter.save(); + painter.setPen( Qt::NoPen ); + painter.setBrush( selectionColor ); + + // Zeichne Rechtecke für alle Fragmente + for ( const auto& fragment : m_currentSelection.fragments ) + { + const auto& pos = fragment.pos; + + // Berechne präzise Breite basierend auf Zeichen-Offsets + float charWidth = 0.0f; + if ( pos.width > 0 && fragment.text.length() > 0 ) + { + charWidth = static_cast( pos.width ) / static_cast( fragment.text.length() ); + } + + int startX = pos.x + static_cast( charWidth * fragment.start_char_offset ); + int width = static_cast( charWidth * ( fragment.end_char_offset - fragment.start_char_offset ) ); + + QRect selectionRect( startX, pos.y, width, pos.height ); + painter.fillRect( selectionRect, selectionColor ); + + // Optional: Rahmen zeichnen + painter.setPen( QPen( borderColor, 1 ) ); + painter.drawRect( selectionRect ); + painter.setPen( Qt::NoPen ); + } + + painter.restore(); +} diff --git a/src/container_qt.h b/src/container_qt.h index 3101166..22262e0 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -1,6 +1,7 @@ #pragma once #include "litehtml.h" +#include "TextHelpers.h" #include "browserdefinitions.h" #include @@ -40,6 +41,11 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void setHighlightColor( const QColor& color ) { mHighlightColor = color; } QColor highlightColor() const { return mHighlightColor; } + void copySelectionToClipboard(); + QString getSelectedText() const; + const TextHelpers::SelectionRange& getCurrentSelection() const; + void clearSelection(); + protected: void paintEvent( QPaintEvent* ) override; void wheelEvent( QWheelEvent* ) override; @@ -50,6 +56,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co Q_SIGNALS: void anchorClicked( const QUrl& ); void scaleChanged(); + void selectionChanged( const QString& selectedText ); protected: litehtml::uint_ptr create_font( @@ -84,8 +91,12 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void get_language( litehtml::string& language, litehtml::string& culture ) const override; void resizeEvent( QResizeEvent* event ) override; bool event( QEvent* event ) override; + void keyPressEvent( QKeyEvent* event ) override; private: + // selection + void drawSelection( QPainter& painter ); + void applyClip( QPainter* p ); bool checkClipRect( QPainter* p, const QRect& rect ) const; @@ -116,62 +127,53 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co MousePos convertMousePos( const QMouseEvent* event ); private: - struct TextFragment - { - std::string text; - litehtml::element::ptr element; - litehtml::position pos; - int start_offset; // Offset in element text - int end_offset; // end Offset in element text - }; - - struct TextFindMatch - { - std::string matched_text; - std::vector fragments; // may contains multiple elements - litehtml::position bounding_box; // bounding box over all elements - }; - - int find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); - bool find_next_match(); - bool find_previous_match(); - const TextFindMatch* find_current_match() const; - void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); - void draw_highlights( litehtml::uint_ptr hdc ); - void clear_highlights() { mFindMatches.clear(); } - std::string normalizeWhitespace( const std::string& text ); - void find_text_in_document( litehtml::document::ptr doc, - const std::string& search_term, - std::vector& matches, - bool case_sensitive = true ); - void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); - litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, - int searchStart, - int searchEnd, - std::vector& matchedFragments ); - void scroll_to_find_match( const TextFindMatch* ); - - std::shared_ptr mDocument; - QByteArray mDocumentSource; - QUrl mBaseUrl; - QUrl mSourceUrl; - int mFontSize = 12; - double mScale = 1.0; - double mMinScale = 0.1; - double mMaxScale = 4.0; - QHash mPixmapCache = {}; - Browser::ResourceHandlerType mResourceHandler = {}; - bool mOpenLinks = true; - bool mOpenExternLinks = false; - QByteArray mFontInfo = {}; - QString mCaption = {}; - Browser::UrlResolveHandlerType mUrlResolverHandler = {}; - QString mMasterCSS; - QString mUserCSS; - QStack mClipStack; - litehtml::position mClip = {}; - std::vector mFindMatches = {}; - int mFindCurrentMatchIndex = -1; - QColor mHighlightColor = QColor( 255, 255, 0, 30 ); - QString mFindText; + int find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); + bool find_next_match(); + bool find_previous_match(); + const TextHelpers::TextFindMatch* find_current_match() const; + void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); + void draw_highlights( litehtml::uint_ptr hdc ); + void clear_highlights() { mFindMatches.clear(); } + std::string normalizeWhitespace( const std::string& text ); + void find_text_in_document( litehtml::document::ptr doc, + const std::string& search_term, + std::vector& matches, + bool case_sensitive = true ); + void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); + litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, + int searchStart, + int searchEnd, + std::vector& matchedFragments ); + void scroll_to_find_match( const TextHelpers::TextFindMatch* ); + + std::shared_ptr mDocument; + QByteArray mDocumentSource; + QUrl mBaseUrl; + QUrl mSourceUrl; + int mFontSize = 12; + double mScale = 1.0; + double mMinScale = 0.1; + double mMaxScale = 4.0; + QHash mPixmapCache = {}; + Browser::ResourceHandlerType mResourceHandler = {}; + bool mOpenLinks = true; + bool mOpenExternLinks = false; + QByteArray mFontInfo = {}; + QString mCaption = {}; + Browser::UrlResolveHandlerType mUrlResolverHandler = {}; + QString mMasterCSS; + QString mUserCSS; + QStack mClipStack; + litehtml::position mClip = {}; + std::vector mFindMatches = {}; + int mFindCurrentMatchIndex = -1; + QColor mHighlightColor = QColor( 255, 255, 0, 30 ); + QString mFindText; + + // for selection + + TextHelpers::DOMTextManager m_textManager; + TextHelpers::SelectionRange m_currentSelection; + bool m_isSelecting; + TextHelpers::TextPosition m_selectionStart; }; diff --git a/test/browser/testbrowser.cpp b/test/browser/testbrowser.cpp index c9e50a5..b771e94 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -107,8 +107,16 @@ TestBrowser::TestBrowser() mScale->setText( mScaleText.arg( static_cast( std::round( mBrowser->scale() * 100.0 ) ) ) ); statusbar->addPermanentWidget( mScale ); + mSelection = new QLabel(); + mSelection->setText( QString() ); + + statusbar->addWidget( mSelection ); + connect( mBrowser, &QHelpBrowser::scaleChanged, this, [this]() { mScale->setText( mScaleText.arg( static_cast( std::round( mBrowser->scale() * 100.0 ) ) ) ); } ); + + connect( mBrowser, &QHelpBrowser::selectionChanged, this, + [this]( const QString& text ) { mSelection->setText( mSelectionText.arg( text.length() ) ); } ); } TestBrowser::~TestBrowser() diff --git a/test/browser/testbrowser.h b/test/browser/testbrowser.h index f3a6485..9065603 100644 --- a/test/browser/testbrowser.h +++ b/test/browser/testbrowser.h @@ -53,4 +53,6 @@ class TestBrowser : public QMainWindow QAction* mPreviousFindMatch = nullptr; QLabel* mScale = nullptr; QString mScaleText = tr( "Zoom: %1%" ); + QLabel* mSelection = nullptr; + QString mSelectionText = tr( "Selected %1 chars" ); }; From b29da16e745cd647f450e1ea20f27d930dfe0876 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 29 Oct 2025 06:42:44 +0100 Subject: [PATCH 02/15] search and select use unique TextManager --- src/CMakeLists.txt | 2 +- src/{TextHelpers.h => DOMTextManager.h} | 242 +++++++++++---- src/container_qt.cpp | 389 ++++++++++++------------ src/container_qt.h | 38 +-- 4 files changed, 406 insertions(+), 265 deletions(-) rename src/{TextHelpers.h => DOMTextManager.h} (50%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64188db..5e0d533 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,7 +22,7 @@ target_sources(QLiteHtmlBrowser QLiteHtmlBrowser.cpp QLiteHtmlBrowserImpl.cpp QLiteHtmlBrowserImpl.h - TextHelpers.h + DOMTextManager.h container_qt.cpp container_qt.h browserdefinitions.h diff --git a/src/TextHelpers.h b/src/DOMTextManager.h similarity index 50% rename from src/TextHelpers.h rename to src/DOMTextManager.h index 5fa95a0..73cbb50 100644 --- a/src/TextHelpers.h +++ b/src/DOMTextManager.h @@ -3,67 +3,67 @@ #include #include -namespace TextHelpers -{ - -struct TextFragment -{ - std::string text; - litehtml::element::ptr element; - litehtml::position pos; - int start_char_offset; // Offset in element text - int end_char_offset; // end Offset in element text - int global_start_offset; - int global_end_offset; - bool is_leaf; - - bool hasValidPosition() const { return pos.width > 0 && pos.height > 0; } -}; - -struct TextFindMatch +class DOMTextManager { - std::string matched_text; - std::vector fragments; // may contains multiple elements - litehtml::position bounding_box; // bounding box over all elements -}; +public: + struct TextFragment + { + std::string text; + litehtml::element::ptr element; + litehtml::position pos; + int start_char_offset; // Offset in element text + int end_char_offset; // end Offset in element text + int global_start_offset; + int global_end_offset; + bool is_leaf; + + bool hasValidPosition() const { return pos.width > 0 && pos.height > 0; } + }; + + struct TextFindMatch + { + std::string matched_text; + std::vector fragments; // may contains multiple elements + litehtml::position bounding_box; // bounding box over all elements -struct TextPosition -{ - litehtml::element::ptr element; - int char_offset; // Zeichenposition im Element-Text - litehtml::position element_pos; + bool isEmpty() const { return fragments.empty(); } + }; - bool operator<( const TextPosition& other ) const + struct TextPosition { - // Vergleich basierend auf Position im Dokument - if ( element_pos.y != other.element_pos.y ) + litehtml::element::ptr element; + int char_offset; // Zeichenposition im Element-Text + litehtml::position element_pos; + + bool operator<( const TextPosition& other ) const { - return element_pos.y < other.element_pos.y; + // Vergleich basierend auf Position im Dokument + if ( element_pos.y != other.element_pos.y ) + { + return element_pos.y < other.element_pos.y; + } + return element_pos.x < other.element_pos.x; } - return element_pos.x < other.element_pos.x; - } - bool isValid() const { return element != nullptr; } -}; + bool isValid() const { return element != nullptr; } + }; -struct SelectionRange -{ - TextPosition start; - TextPosition end; - std::vector fragments; // Alle Textfragmente in der Selektion + struct SelectionRange + { + TextPosition start; + TextPosition end; + std::vector fragments; // Alle Textfragmente in der Selektion - bool isEmpty() const { return !start.isValid() || !end.isValid(); } + bool isEmpty() const { return !start.isValid() || !end.isValid(); } - void clear() - { - start = TextPosition(); - end = TextPosition(); - fragments.clear(); - } -}; + void clear() + { + start = TextPosition(); + end = TextPosition(); + fragments.clear(); + } + }; -class DOMTextManager -{ private: std::vector m_fragments; std::string m_fullText; @@ -229,6 +229,52 @@ class DOMTextManager } const std::vector& getFragments() const { return m_fragments; } + + std::vector findText( const std::string& search_term, bool case_sensitive = true ) + { + std::vector results; + + if ( search_term.empty() || m_fullText.empty() ) + { + return results; + } + + // Normalisiere Text + std::string normalizedFullText = ( m_fullText ); + std::string normalizedSearchTerm = normalizeWhitespace( search_term ); + + std::string searchText = normalizedFullText; + std::string searchFor = normalizedSearchTerm; + + if ( !case_sensitive ) + { + std::transform( searchText.begin(), searchText.end(), searchText.begin(), ::tolower ); + std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); + } + + // Alle Vorkommen finden + size_t pos = 0; + while ( ( pos = searchText.find( searchFor, pos ) ) != std::string::npos ) + { + TextFindMatch result; + result.matched_text = normalizedFullText.substr( pos, searchFor.length() ); + + int searchStart = static_cast( pos ); + int searchEnd = static_cast( pos + searchFor.length() ); + + // Finde alle Fragmente in diesem Bereich + result.bounding_box = calculateBoundingBox( searchStart, searchEnd, result.fragments ); + // result.bounding_box = calculatePreciseBoundingBox( searchStart, searchEnd, result.fragments ); + + results.push_back( result ); + pos += searchFor.length(); + } + + qDebug() << "Suche nach '" << QString::fromStdString( search_term ) << "': " << results.size() << " Treffer"; + + return results; + } + private: std::string normalizeWhitespace( const std::string& text ) { @@ -301,5 +347,99 @@ class DOMTextManager collectTextFragments( *it ); } } + + litehtml::position calculateBoundingBox( int searchStart, int searchEnd, std::vector& matchedFragments ) + { + litehtml::position boundingBox = { 0, 0, 0, 0 }; + bool firstFragment = true; + + qDebug() << "\n=== Berechne Bounding Box ==="; + qDebug() << "Suchbereich: global offset" << searchStart << "bis" << searchEnd; + qDebug() << "Gesuchter Text:" + << QString::fromStdString( + m_fullText.substr( searchStart, std::min( (size_t)( searchEnd - searchStart ), m_fullText.length() - searchStart ) ) ); + + for ( const auto& fragment : m_fragments ) + { + if ( !fragment.is_leaf || !fragment.hasValidPosition() ) + { + continue; + } + + // Überlappung zwischen Suchbereich und Fragment + int overlapStart = std::max( searchStart, fragment.global_start_offset ); + int overlapEnd = std::min( searchEnd, fragment.global_end_offset ); + + if ( overlapStart < overlapEnd ) + { + // Kopie des Fragments erstellen + TextFragment matchedFragment = fragment; + + // Lokale Offsets im Fragment-Text berechnen + int localStart = overlapStart - fragment.global_start_offset; + int localEnd = overlapEnd - fragment.global_start_offset; + + // WICHTIG: Bounds checking + int maxLen = static_cast( fragment.text.length() ); + localStart = std::clamp( localStart, 0, maxLen ); + localEnd = std::clamp( localEnd, 0, maxLen ); + + if ( localEnd <= localStart ) + { + qDebug() << "Warnung: Ungültige Offsets für Fragment"; + continue; + } + + matchedFragment.start_char_offset = localStart; + matchedFragment.end_char_offset = localEnd; + + // WICHTIG: Position des matched Fragments anpassen + litehtml::position adjustedPos = fragment.pos; + + if ( maxLen > 0 && fragment.pos.width > 0 ) + { + float charWidth = static_cast( fragment.pos.width ) / static_cast( maxLen ); + + // X-Position anpassen basierend auf localStart + adjustedPos.x = fragment.pos.x + static_cast( charWidth * localStart ); + + // Breite anpassen basierend auf Anzahl der Zeichen + adjustedPos.width = static_cast( charWidth * ( localEnd - localStart ) ); + } + + // Speichere die angepasste Position IM Fragment + matchedFragment.pos = adjustedPos; + + matchedFragments.push_back( matchedFragment ); + + qDebug() << "Fragment gefunden:" << QString::fromStdString( fragment.text.substr( localStart, localEnd - localStart ) ) + << "| Local:" << localStart << "-" << localEnd << "| Pos:" << adjustedPos.x << adjustedPos.y << "| Size:" << adjustedPos.width << "x" + << adjustedPos.height; + + // Bounding Box erweitern + if ( firstFragment ) + { + boundingBox = adjustedPos; + firstFragment = false; + } + else + { + int minX = std::min( boundingBox.x, adjustedPos.x ); + int minY = std::min( boundingBox.y, adjustedPos.y ); + int maxX = std::max( boundingBox.x + boundingBox.width, adjustedPos.x + adjustedPos.width ); + int maxY = std::max( boundingBox.y + boundingBox.height, adjustedPos.y + adjustedPos.height ); + + boundingBox.x = minX; + boundingBox.y = minY; + boundingBox.width = maxX - minX; + boundingBox.height = maxY - minY; + } + } + } + + qDebug() << "Finale Bounding Box:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; + qDebug() << "=========================\n"; + + return boundingBox; + } }; -} diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 3523835..be04e5e 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -22,8 +22,6 @@ #include #include -using namespace TextHelpers; - static const auto CSS_GENERIC_FONT_TO_QFONT_STYLEHINT = QMap{ { "serif", QFont::StyleHint::Serif }, { "sans-serif", QFont::StyleHint::SansSerif }, { "monospace", QFont::StyleHint::Monospace }, @@ -1001,7 +999,7 @@ void container_qt::mouseMoveEvent( QMouseEvent* e ) } if ( m_isSelecting && m_selectionStart.isValid() ) { - TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.client.x(), mousePos.client.y() ); + DOMTextManager::TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.client.x(), mousePos.client.y() ); if ( currentPos.isValid() ) { @@ -1038,7 +1036,7 @@ void container_qt::mouseReleaseEvent( QMouseEvent* e ) qDebug() << getSelectedText(); qDebug() << "================================\n"; emit selectionChanged( getSelectedText() ); - copySelectionToClipboard(); + // copySelectionToClipboard(); } } // else @@ -1130,187 +1128,187 @@ int container_qt::findText( const QString& text ) return mFindMatches.size(); } -// normalize Whitespace: multiple whitespaces to one -std::string container_qt::normalizeWhitespace( const std::string& text ) -{ - std::string normalized; - bool lastWasSpace = false; - - for ( char c : text ) - { - if ( std::isspace( static_cast( c ) ) ) - { - if ( !lastWasSpace && !normalized.empty() ) - { - normalized += ' '; - lastWasSpace = true; - } - } - else - { - normalized += c; - lastWasSpace = false; - } - } - - return normalized; -} - -void container_qt::collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ) -{ - if ( !el ) - return; - - std::string text; - el->get_text( text ); - if ( !text.empty() && el->is_text() ) - { - TextFragment fragment; - fragment.text = text; - fragment.element = el; - fragment.pos = el->get_placement(); - fragment.start_char_offset = static_cast( fullText.length() ); - - std::string normalized = normalizeWhitespace( text ); - fullText += normalized; - - fragment.end_char_offset = static_cast( fullText.length() ); - fragments.push_back( fragment ); - - // Leerzeichen zwischen Elementen - if ( !fullText.empty() && !std::isspace( fullText.back() ) ) - { - fullText += ' '; - } - } - - for ( auto it = el->children().begin(); it != el->children().end(); ++it ) - { - collect_text_fragments( ( *it ), fragments, fullText ); - } -} - -// calculate bounding box for all elements found in search -litehtml::position container_qt::calculate_precise_bounding_box( const std::vector& allFragments, - int searchStart, - int searchEnd, - std::vector& matchedFragments ) -{ - litehtml::position boundingBox = { 0, 0, 0, 0 }; - bool firstFragment = true; - - for ( const auto& fragment : allFragments ) - { - // check if this fragment is part of the search - int overlapStart = std::max( searchStart, fragment.start_char_offset ); - int overlapEnd = std::min( searchEnd, fragment.end_char_offset ); - - if ( overlapStart < overlapEnd ) - { - // yes this fragment is part of the search - TextFragment matchedFragment = fragment; - - // offsets relative to start - int fragmentTextStart = overlapStart - fragment.start_char_offset; - int fragmentTextEnd = overlapEnd - fragment.start_char_offset; - - matchedFragment.start_char_offset = fragmentTextStart; - matchedFragment.end_char_offset = fragmentTextEnd; - - // calculate width of text - // Importnat: use text_width from document_container - std::string matchedText = fragment.text.substr( fragmentTextStart, fragmentTextEnd - fragmentTextStart ); - - // estimated position (kann mit text_width verfeinert werden) - litehtml::position fragmentPos = fragment.pos; - - // for more precise positioning text_width would be helpfull - // int textWidthBefore = container->text_width( - // fragment.text.substr(0, fragmentTextStart).c_str(), - // font - // ); - // fragmentPos.x += textWidthBefore; - // fragmentPos.width = container->text_width(matchedText.c_str(), font); - - // simplified approximation - if ( fragment.text.length() > 0 ) - { - float charWidth = static_cast( fragment.pos.width ) / static_cast( fragment.text.length() ); - fragmentPos.x += static_cast( charWidth * fragmentTextStart ); - fragmentPos.width = static_cast( charWidth * matchedText.length() ); - } - - matchedFragments.push_back( matchedFragment ); - - // extend Bounding Box - if ( firstFragment ) - { - boundingBox = fragmentPos; - firstFragment = false; - } - else - { - // Min/Max for containing Box - int minX = std::min( boundingBox.x, fragmentPos.x ); - int minY = std::min( boundingBox.y, fragmentPos.y ); - int maxX = std::max( boundingBox.x + boundingBox.width, fragmentPos.x + fragmentPos.width ); - int maxY = std::max( boundingBox.y + boundingBox.height, fragmentPos.y + fragmentPos.height ); - - boundingBox.x = minX; - boundingBox.y = minY; - boundingBox.width = maxX - minX; - boundingBox.height = maxY - minY; - } - } - } - - return boundingBox; -} - -// search document for Text including multiword phrases -void container_qt::find_text_in_document( litehtml::document::ptr doc, - const std::string& search_term, - std::vector& matches, - bool case_sensitive ) -{ - if ( !doc || search_term.empty() ) - return; - - // Sammle alle Text-Fragmente mit Positionen - std::vector fragments; - std::string fullText; - collect_text_fragments( doc->root(), fragments, fullText ); - - // Normalisiere Text für Suche - std::string normalizedFullText = normalizeWhitespace( fullText ); - std::string normalizedSearchTerm = normalizeWhitespace( search_term ); - - std::string findText = normalizedFullText; - std::string searchFor = normalizedSearchTerm; - - if ( !case_sensitive ) - { - std::transform( findText.begin(), findText.end(), findText.begin(), ::tolower ); - std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); - } - - // Alle Vorkommen finden - size_t pos = 0; - while ( ( pos = findText.find( searchFor, pos ) ) != std::string::npos ) - { - TextFindMatch match; - match.matched_text = normalizedFullText.substr( pos, searchFor.length() ); - - int searchStart = static_cast( pos ); - int searchEnd = static_cast( pos + searchFor.length() ); - - // Berechne präzise Bounding Box - match.bounding_box = calculate_precise_bounding_box( fragments, searchStart, searchEnd, match.fragments ); - - matches.push_back( match ); - pos += searchFor.length(); - } -} +// // normalize Whitespace: multiple whitespaces to one +// std::string container_qt::normalizeWhitespace( const std::string& text ) +// { +// std::string normalized; +// bool lastWasSpace = false; + +// for ( char c : text ) +// { +// if ( std::isspace( static_cast( c ) ) ) +// { +// if ( !lastWasSpace && !normalized.empty() ) +// { +// normalized += ' '; +// lastWasSpace = true; +// } +// } +// else +// { +// normalized += c; +// lastWasSpace = false; +// } +// } + +// return normalized; +// } + +// void container_qt::collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ) +// { +// if ( !el ) +// return; + +// std::string text; +// el->get_text( text ); +// if ( !text.empty() && el->is_text() ) +// { +// TextFragment fragment; +// fragment.text = text; +// fragment.element = el; +// fragment.pos = el->get_placement(); +// fragment.start_char_offset = static_cast( fullText.length() ); + +// std::string normalized = normalizeWhitespace( text ); +// fullText += normalized; + +// fragment.end_char_offset = static_cast( fullText.length() ); +// fragments.push_back( fragment ); + +// // Leerzeichen zwischen Elementen +// if ( !fullText.empty() && !std::isspace( fullText.back() ) ) +// { +// fullText += ' '; +// } +// } + +// for ( auto it = el->children().begin(); it != el->children().end(); ++it ) +// { +// collect_text_fragments( ( *it ), fragments, fullText ); +// } +// } + +// // calculate bounding box for all elements found in search +// litehtml::position container_qt::calculate_precise_bounding_box( const std::vector& allFragments, +// int searchStart, +// int searchEnd, +// std::vector& matchedFragments ) +// { +// litehtml::position boundingBox = { 0, 0, 0, 0 }; +// bool firstFragment = true; + +// for ( const auto& fragment : allFragments ) +// { +// // check if this fragment is part of the search +// int overlapStart = std::max( searchStart, fragment.start_char_offset ); +// int overlapEnd = std::min( searchEnd, fragment.end_char_offset ); + +// if ( overlapStart < overlapEnd ) +// { +// // yes this fragment is part of the search +// TextFragment matchedFragment = fragment; + +// // offsets relative to start +// int fragmentTextStart = overlapStart - fragment.start_char_offset; +// int fragmentTextEnd = overlapEnd - fragment.start_char_offset; + +// matchedFragment.start_char_offset = fragmentTextStart; +// matchedFragment.end_char_offset = fragmentTextEnd; + +// // calculate width of text +// // Importnat: use text_width from document_container +// std::string matchedText = fragment.text.substr( fragmentTextStart, fragmentTextEnd - fragmentTextStart ); + +// // estimated position (kann mit text_width verfeinert werden) +// litehtml::position fragmentPos = fragment.pos; + +// // for more precise positioning text_width would be helpfull +// // int textWidthBefore = container->text_width( +// // fragment.text.substr(0, fragmentTextStart).c_str(), +// // font +// // ); +// // fragmentPos.x += textWidthBefore; +// // fragmentPos.width = container->text_width(matchedText.c_str(), font); + +// // simplified approximation +// if ( fragment.text.length() > 0 ) +// { +// float charWidth = static_cast( fragment.pos.width ) / static_cast( fragment.text.length() ); +// fragmentPos.x += static_cast( charWidth * fragmentTextStart ); +// fragmentPos.width = static_cast( charWidth * matchedText.length() ); +// } + +// matchedFragments.push_back( matchedFragment ); + +// // extend Bounding Box +// if ( firstFragment ) +// { +// boundingBox = fragmentPos; +// firstFragment = false; +// } +// else +// { +// // Min/Max for containing Box +// int minX = std::min( boundingBox.x, fragmentPos.x ); +// int minY = std::min( boundingBox.y, fragmentPos.y ); +// int maxX = std::max( boundingBox.x + boundingBox.width, fragmentPos.x + fragmentPos.width ); +// int maxY = std::max( boundingBox.y + boundingBox.height, fragmentPos.y + fragmentPos.height ); + +// boundingBox.x = minX; +// boundingBox.y = minY; +// boundingBox.width = maxX - minX; +// boundingBox.height = maxY - minY; +// } +// } +// } + +// return boundingBox; +// } + +// // search document for Text including multiword phrases +// void container_qt::find_text_in_document( litehtml::document::ptr doc, +// const std::string& search_term, +// std::vector& matches, +// bool case_sensitive ) +// { +// if ( !doc || search_term.empty() ) +// return; + +// // Sammle alle Text-Fragmente mit Positionen +// std::vector fragments; +// std::string fullText; +// collect_text_fragments( doc->root(), fragments, fullText ); + +// // Normalisiere Text für Suche +// std::string normalizedFullText = normalizeWhitespace( fullText ); +// std::string normalizedSearchTerm = normalizeWhitespace( search_term ); + +// std::string findText = normalizedFullText; +// std::string searchFor = normalizedSearchTerm; + +// if ( !case_sensitive ) +// { +// std::transform( findText.begin(), findText.end(), findText.begin(), ::tolower ); +// std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); +// } + +// // Alle Vorkommen finden +// size_t pos = 0; +// while ( ( pos = findText.find( searchFor, pos ) ) != std::string::npos ) +// { +// TextFindMatch match; +// match.matched_text = normalizedFullText.substr( pos, searchFor.length() ); + +// int searchStart = static_cast( pos ); +// int searchEnd = static_cast( pos + searchFor.length() ); + +// // Berechne präzise Bounding Box +// match.bounding_box = calculate_precise_bounding_box( fragments, searchStart, searchEnd, match.fragments ); + +// matches.push_back( match ); +// pos += searchFor.length(); +// } +// } // Textsuche durchführen int container_qt::find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive ) @@ -1323,8 +1321,7 @@ int container_qt::find_text( litehtml::document::ptr doc, const std::string& sea return 0; } - // Suche vom Root-Element starten - find_text_in_document( mDocument, search_term, mFindMatches, case_sensitive ); + mFindMatches = m_textManager.findText( search_term, case_sensitive ); if ( !mFindMatches.empty() ) { @@ -1363,7 +1360,7 @@ bool container_qt::find_previous_match() } // Aktuelles Suchergebnis mit Position holen -const TextHelpers::TextFindMatch* container_qt::find_current_match() const +const DOMTextManager::TextFindMatch* container_qt::find_current_match() const { if ( mFindCurrentMatchIndex >= 0 && mFindCurrentMatchIndex < static_cast( mFindMatches.size() ) ) { @@ -1380,17 +1377,17 @@ void container_qt::draw_highlights( litehtml::uint_ptr hdc ) for ( auto it = match.fragments.begin(); it != match.fragments.end(); ++it ) { litehtml::position pos = ( *it ).pos; - highlight_text_at_position( hdc, pos, match.matched_text ); + highlight_text_at_position( hdc, pos, match ); } } } // draw highlighted text at position -void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ) +void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const DOMTextManager::TextFindMatch& match ) { - qDebug() << "Highlighting text '" << QString::fromStdString( text ) << "' at position (" << pos.x << ", " << pos.y << ") with size " << pos.width - << "x" << pos.height; + qDebug() << "Highlighting text '" << QString::fromStdString( match.matched_text ) << "' at position (" << pos.x << ", " << pos.y << ") with size " + << pos.width << "x" << pos.height; QPainter* p( reinterpret_cast( hdc ) ); p->save(); @@ -1403,6 +1400,10 @@ void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const lit p->drawRect( ( QRect( pos.x + scroll_pos.x(), pos.y + scroll_pos.y(), pos.width, pos.height ) ) ); + // p->setPen( QPen( Qt::green, 3, Qt::DotLine ) ); + // p->setBrush( Qt::NoBrush ); + // p->drawRect( match.bounding_box.x + scroll_pos.x(), match.bounding_box.y + scroll_pos.y(), match.bounding_box.width, match.bounding_box.height ); + p->restore(); } @@ -1424,7 +1425,7 @@ void container_qt::findPreviousMatch() } } -void container_qt::scroll_to_find_match( const TextFindMatch* match ) +void container_qt::scroll_to_find_match( const DOMTextManager::TextFindMatch* match ) { if ( !match ) { @@ -1488,7 +1489,7 @@ QString container_qt::getSelectedText() const return result; } -const SelectionRange& container_qt::getCurrentSelection() const +const DOMTextManager::SelectionRange& container_qt::getCurrentSelection() const { return m_currentSelection; } diff --git a/src/container_qt.h b/src/container_qt.h index 22262e0..aa1a30d 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -1,7 +1,7 @@ #pragma once #include "litehtml.h" -#include "TextHelpers.h" +#include "DOMTextManager.h" #include "browserdefinitions.h" #include @@ -43,7 +43,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void copySelectionToClipboard(); QString getSelectedText() const; - const TextHelpers::SelectionRange& getCurrentSelection() const; + const DOMTextManager::SelectionRange& getCurrentSelection() const; void clearSelection(); protected: @@ -130,21 +130,21 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co int find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); bool find_next_match(); bool find_previous_match(); - const TextHelpers::TextFindMatch* find_current_match() const; - void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const std::string& text ); + const DOMTextManager::TextFindMatch* find_current_match() const; + void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const DOMTextManager::TextFindMatch& match ); void draw_highlights( litehtml::uint_ptr hdc ); void clear_highlights() { mFindMatches.clear(); } - std::string normalizeWhitespace( const std::string& text ); - void find_text_in_document( litehtml::document::ptr doc, - const std::string& search_term, - std::vector& matches, - bool case_sensitive = true ); - void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ); - litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, - int searchStart, - int searchEnd, - std::vector& matchedFragments ); - void scroll_to_find_match( const TextHelpers::TextFindMatch* ); + // std::string normalizeWhitespace( const std::string& text ); + // void find_text_in_document( litehtml::document::ptr doc, + // const std::string& search_term, + // std::vector& matches, + // bool case_sensitive = true ); + // void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText + // ); litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, + // int searchStart, + // int searchEnd, + // std::vector& matchedFragments ); + void scroll_to_find_match( const DOMTextManager::TextFindMatch* ); std::shared_ptr mDocument; QByteArray mDocumentSource; @@ -165,15 +165,15 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co QString mUserCSS; QStack mClipStack; litehtml::position mClip = {}; - std::vector mFindMatches = {}; + std::vector mFindMatches = {}; int mFindCurrentMatchIndex = -1; QColor mHighlightColor = QColor( 255, 255, 0, 30 ); QString mFindText; // for selection - TextHelpers::DOMTextManager m_textManager; - TextHelpers::SelectionRange m_currentSelection; + DOMTextManager m_textManager; + DOMTextManager::SelectionRange m_currentSelection; bool m_isSelecting; - TextHelpers::TextPosition m_selectionStart; + DOMTextManager::TextPosition m_selectionStart; }; From d4caf8faaa4e9396313407b27607a24942dc1e00 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 30 Oct 2025 07:00:10 +0100 Subject: [PATCH 03/15] selectin working, multiline search broken --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 4 +- src/DOMTextManager.h | 53 +++++++++++---------- src/QLiteHtmlBrowser.cpp | 6 +++ src/QLiteHtmlBrowserImpl.cpp | 9 ++++ src/QLiteHtmlBrowserImpl.h | 4 +- src/container_qt.cpp | 43 +++++++++-------- src/container_qt.h | 15 +++--- test/browser/testbrowser.cpp | 6 ++- 8 files changed, 83 insertions(+), 57 deletions(-) diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 0c0ca5b..7eae884 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -155,6 +155,8 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget QColor highlightColor() const; void setHighlightColor( QColor color ); + QString selectedText() const; + public Q_SLOTS: /// set URL to given url. The URL may be an url to local file, QtHelp, http etc. /// The URL could contain an anchor element. Currently parameters to URL like @@ -189,7 +191,7 @@ public Q_SLOTS: void scaleChanged(); /// send when selection changes - void selectionChanged( const QString& selectedText ); + void selectionChanged(); protected: private: diff --git a/src/DOMTextManager.h b/src/DOMTextManager.h index 73cbb50..8a08ddc 100644 --- a/src/DOMTextManager.h +++ b/src/DOMTextManager.h @@ -78,16 +78,16 @@ class DOMTextManager { collectTextFragments( doc->root() ); - qDebug() << "\n=== Gesammelte Text-Fragmente ==="; - qDebug() << "Anzahl Fragmente:" << m_fragments.size(); - for ( size_t i = 0; i < m_fragments.size(); ++i ) - { - const auto& frag = m_fragments[i]; - qDebug() << "Fragment" << i << ":" - << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y - << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); - } - qDebug() << "================================\n"; + // qDebug() << "\n=== Gesammelte Text-Fragmente ==="; + // qDebug() << "Anzahl Fragmente:" << m_fragments.size(); + // for ( size_t i = 0; i < m_fragments.size(); ++i ) + // { + // const auto& frag = m_fragments[i]; + // qDebug() << "Fragment" << i << ":" + // << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y + // << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); + // } + // qDebug() << "================================\n"; } } @@ -98,7 +98,7 @@ class DOMTextManager TextFragment* bestMatch = nullptr; int smallestArea = INT_MAX; - qDebug() << "\n>>> Suche Element bei:" << x << y; + // qDebug() << "\n>>> Suche Element bei:" << x << y; // Suche das kleinste Element, das den Punkt enthält (spezifischstes Element) for ( auto& fragment : m_fragments ) @@ -117,8 +117,8 @@ class DOMTextManager int area = pos.width * pos.height; - qDebug() << " Kandidat:" << QString::fromStdString( fragment.text ).left( 20 ) << "| Box:" << pos.x << pos.y << pos.width << pos.height - << "| Area:" << area; + // qDebug() << " Kandidat:" << QString::fromStdString( fragment.text ).left( 20 ) << "| Box:" << pos.x << pos.y << pos.width << pos.height + // << "| Area:" << area; // Wähle das kleinste umschließende Element if ( area < smallestArea ) @@ -223,7 +223,7 @@ class DOMTextManager } } - qDebug() << "Selektion umfasst" << selection.fragments.size() << "Fragmente"; + // qDebug() << "Selektion umfasst" << selection.fragments.size() << "Fragmente"; return selection; } @@ -336,8 +336,8 @@ class DOMTextManager m_fragments.push_back( fragment ); - qDebug() << "Leaf-Fragment:" << QString::fromStdString( fragment.text ).left( 30 ) << "| Pos:" << pos.x << pos.y << "| Size:" << pos.width - << "x" << pos.height; + // qDebug() << "Leaf-Fragment:" << QString::fromStdString( fragment.text ).left( 30 ) << "| Pos:" << pos.x << pos.y << "| Size:" << pos.width + // << "x" << pos.height; } } @@ -353,11 +353,11 @@ class DOMTextManager litehtml::position boundingBox = { 0, 0, 0, 0 }; bool firstFragment = true; - qDebug() << "\n=== Berechne Bounding Box ==="; - qDebug() << "Suchbereich: global offset" << searchStart << "bis" << searchEnd; - qDebug() << "Gesuchter Text:" - << QString::fromStdString( - m_fullText.substr( searchStart, std::min( (size_t)( searchEnd - searchStart ), m_fullText.length() - searchStart ) ) ); + // qDebug() << "\n=== Berechne Bounding Box ==="; + // qDebug() << "Suchbereich: global offset" << searchStart << "bis" << searchEnd; + // qDebug() << "Gesuchter Text:" + // << QString::fromStdString( + // m_fullText.substr( searchStart, std::min( (size_t)( searchEnd - searchStart ), m_fullText.length() - searchStart ) ) ); for ( const auto& fragment : m_fragments ) { @@ -412,9 +412,10 @@ class DOMTextManager matchedFragments.push_back( matchedFragment ); - qDebug() << "Fragment gefunden:" << QString::fromStdString( fragment.text.substr( localStart, localEnd - localStart ) ) - << "| Local:" << localStart << "-" << localEnd << "| Pos:" << adjustedPos.x << adjustedPos.y << "| Size:" << adjustedPos.width << "x" - << adjustedPos.height; + // qDebug() << "Fragment gefunden:" << QString::fromStdString( fragment.text.substr( localStart, localEnd - localStart ) ) + // << "| Local:" << localStart << "-" << localEnd << "| Pos:" << adjustedPos.x << adjustedPos.y << "| Size:" << adjustedPos.width << + // "x" + // << adjustedPos.height; // Bounding Box erweitern if ( firstFragment ) @@ -437,8 +438,8 @@ class DOMTextManager } } - qDebug() << "Finale Bounding Box:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; - qDebug() << "=========================\n"; + // qDebug() << "Finale Bounding Box:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; + // qDebug() << "=========================\n"; return boundingBox; } diff --git a/src/QLiteHtmlBrowser.cpp b/src/QLiteHtmlBrowser.cpp index ec850d4..bd4c801 100644 --- a/src/QLiteHtmlBrowser.cpp +++ b/src/QLiteHtmlBrowser.cpp @@ -19,6 +19,7 @@ QLiteHtmlBrowser::QLiteHtmlBrowser( QWidget* parent ) connect( mImpl, &QLiteHtmlBrowserImpl::urlChanged, this, &QLiteHtmlBrowser::sourceChanged ); connect( mImpl, &QLiteHtmlBrowserImpl::anchorClicked, this, &QLiteHtmlBrowser::anchorClicked ); connect( mImpl, &QLiteHtmlBrowserImpl::scaleChanged, this, &QLiteHtmlBrowser::scaleChanged ); + connect( mImpl, &QLiteHtmlBrowserImpl::selectionChanged, this, &QLiteHtmlBrowser::selectionChanged ); } QUrl QLiteHtmlBrowser::source() const @@ -187,3 +188,8 @@ void QLiteHtmlBrowser::setHighlightColor( QColor color ) { mImpl->setHighlightColor( color ); } + +QString QLiteHtmlBrowser::selectedText() const +{ + return mImpl->selectedText(); +} diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index 55b11b8..cc048e6 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -495,3 +495,12 @@ void QLiteHtmlBrowserImpl::previousFindMatch() mContainer->findPreviousMatch(); } } +QString QLiteHtmlBrowserImpl::selectedText() const +{ + QString text; + if ( mContainer ) + { + text = mContainer->selectedText(); + } + return text; +} diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index dd502f4..c97dac1 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -70,6 +70,8 @@ class QLiteHtmlBrowserImpl : public QWidget QColor highlightColor() const; void setHighlightColor( QColor color ); + QString selectedText() const; + protected: void changeEvent( QEvent* ) override; void mousePressEvent( QMouseEvent* ) override; @@ -79,7 +81,7 @@ class QLiteHtmlBrowserImpl : public QWidget void urlChanged( const QUrl& ); void anchorClicked( const QUrl& ); void scaleChanged(); - void selectionChanged( const QString& selectedText ); + void selectionChanged(); private: class HistoryEntry diff --git a/src/container_qt.cpp b/src/container_qt.cpp index be04e5e..08764d2 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -1003,8 +1003,9 @@ void container_qt::mouseMoveEvent( QMouseEvent* e ) if ( currentPos.isValid() ) { + qDebug() << "selection is valid"; m_currentSelection = m_textManager.getSelectionBetween( m_selectionStart, currentPos ); - update(); + viewport()->update(); } } @@ -1031,11 +1032,11 @@ void container_qt::mouseReleaseEvent( QMouseEvent* e ) if ( !m_currentSelection.isEmpty() ) { - qDebug() << "selection got selected text with " << getSelectedText().count() << "fragments"; + qDebug() << "selection got selected text with " << selectedText().count() << "fragments"; qDebug() << "\n=== Selektierter Text ==="; - qDebug() << getSelectedText(); + qDebug() << selectedText(); qDebug() << "================================\n"; - emit selectionChanged( getSelectedText() ); + emit selectionChanged(); // copySelectionToClipboard(); } } @@ -1442,16 +1443,16 @@ void container_qt::copySelectionToClipboard() return; } - QString selectedText = getSelectedText(); - if ( !selectedText.isEmpty() ) + QString text = selectedText(); + if ( !text.isEmpty() ) { QClipboard* clipboard = QApplication::clipboard(); - clipboard->setText( selectedText ); + clipboard->setText( text ); } } // Gibt selektierten Text zurück -QString container_qt::getSelectedText() const +QString container_qt::selectedText() const { if ( m_currentSelection.isEmpty() ) { @@ -1489,10 +1490,10 @@ QString container_qt::getSelectedText() const return result; } -const DOMTextManager::SelectionRange& container_qt::getCurrentSelection() const -{ - return m_currentSelection; -} +// const DOMTextManager::SelectionRange& container_qt::getCurrentSelection() const +// { +// return m_currentSelection; +// } void container_qt::clearSelection() { @@ -1520,12 +1521,10 @@ void container_qt::drawSelection( QPainter& painter ) { qDebug() << "container_qt::drawSelection"; // Farben für Selektion (Windows-Style) - QColor selectionColor( 0, 120, 215, 100 ); // Blau mit Transparenz - QColor borderColor( 0, 120, 215, 180 ); painter.save(); painter.setPen( Qt::NoPen ); - painter.setBrush( selectionColor ); + painter.setBrush( mSelectionColor ); // Zeichne Rechtecke für alle Fragmente for ( const auto& fragment : m_currentSelection.fragments ) @@ -1542,13 +1541,15 @@ void container_qt::drawSelection( QPainter& painter ) int startX = pos.x + static_cast( charWidth * fragment.start_char_offset ); int width = static_cast( charWidth * ( fragment.end_char_offset - fragment.start_char_offset ) ); - QRect selectionRect( startX, pos.y, width, pos.height ); - painter.fillRect( selectionRect, selectionColor ); + auto scroll_pos = -scrollBarPos(); + + QRect selectionRect( startX + scroll_pos.x(), pos.y + scroll_pos.y(), width, pos.height ); + painter.fillRect( selectionRect, mSelectionColor ); - // Optional: Rahmen zeichnen - painter.setPen( QPen( borderColor, 1 ) ); - painter.drawRect( selectionRect ); - painter.setPen( Qt::NoPen ); + // // Optional: Rahmen zeichnen + // painter.setPen( QPen( borderColor, 1 ) ); + // painter.drawRect( selectionRect ); + // painter.setPen( Qt::NoPen ); } painter.restore(); diff --git a/src/container_qt.h b/src/container_qt.h index aa1a30d..b753d24 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -41,10 +41,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void setHighlightColor( const QColor& color ) { mHighlightColor = color; } QColor highlightColor() const { return mHighlightColor; } - void copySelectionToClipboard(); - QString getSelectedText() const; - const DOMTextManager::SelectionRange& getCurrentSelection() const; - void clearSelection(); + QString selectedText() const; protected: void paintEvent( QPaintEvent* ) override; @@ -56,7 +53,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co Q_SIGNALS: void anchorClicked( const QUrl& ); void scaleChanged(); - void selectionChanged( const QString& selectedText ); + void selectionChanged(); protected: litehtml::uint_ptr create_font( @@ -92,6 +89,9 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void resizeEvent( QResizeEvent* event ) override; bool event( QEvent* event ) override; void keyPressEvent( QKeyEvent* event ) override; + // const DOMTextManager::SelectionRange& getCurrentSelection() const; + void copySelectionToClipboard(); + void clearSelection(); private: // selection @@ -167,8 +167,9 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co litehtml::position mClip = {}; std::vector mFindMatches = {}; int mFindCurrentMatchIndex = -1; - QColor mHighlightColor = QColor( 255, 255, 0, 30 ); - QString mFindText; + QColor mHighlightColor = QColor( 255, 255, 0, 30 ); + QColor mSelectionColor = QColor( 0, 120, 215, 100 ); + QString mFindText; // for selection diff --git a/test/browser/testbrowser.cpp b/test/browser/testbrowser.cpp index b771e94..31be347 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -116,7 +116,11 @@ TestBrowser::TestBrowser() [this]() { mScale->setText( mScaleText.arg( static_cast( std::round( mBrowser->scale() * 100.0 ) ) ) ); } ); connect( mBrowser, &QHelpBrowser::selectionChanged, this, - [this]( const QString& text ) { mSelection->setText( mSelectionText.arg( text.length() ) ); } ); + [this]() + { + auto text = mBrowser->selectedText(); + mSelection->setText( mSelectionText.arg( text.length() ) ); + } ); } TestBrowser::~TestBrowser() From b8b2dba5ff5f053e1f459cdf7d1a4b5cbf353e30 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 30 Oct 2025 08:20:11 +0100 Subject: [PATCH 04/15] best current possible solution --- src/DOMTextManager.h | 184 +++++++++++++++++++------------------------ src/container_qt.cpp | 23 +++--- 2 files changed, 91 insertions(+), 116 deletions(-) diff --git a/src/DOMTextManager.h b/src/DOMTextManager.h index 8a08ddc..6785939 100644 --- a/src/DOMTextManager.h +++ b/src/DOMTextManager.h @@ -23,8 +23,8 @@ class DOMTextManager struct TextFindMatch { std::string matched_text; - std::vector fragments; // may contains multiple elements - litehtml::position bounding_box; // bounding box over all elements + std::vector fragments; + litehtml::position bounding_box; bool isEmpty() const { return fragments.empty(); } }; @@ -32,20 +32,18 @@ class DOMTextManager struct TextPosition { litehtml::element::ptr element; - int char_offset; // Zeichenposition im Element-Text + int char_offset = 0; litehtml::position element_pos; bool operator<( const TextPosition& other ) const { - // Vergleich basierend auf Position im Dokument if ( element_pos.y != other.element_pos.y ) - { return element_pos.y < other.element_pos.y; - } - return element_pos.x < other.element_pos.x; + if ( element_pos.x != other.element_pos.x ) + return element_pos.x < other.element_pos.x; + return char_offset < other.char_offset; } - - bool isValid() const { return element != nullptr; } + bool isValid() const { return element && element_pos.width > 0 && element_pos.height > 0; } }; struct SelectionRange @@ -77,6 +75,14 @@ class DOMTextManager if ( doc && doc->root() ) { collectTextFragments( doc->root() ); + if ( !m_fullText.empty() && m_fullText.back() == ' ' ) + { + m_fullText.pop_back(); + } + + // qDebug() << "\n=== DOM Text Manager initialisiert ==="; + // qDebug() << "Fragmente gesammelt:" << m_fragments.size(); + // qDebug() << "Gesamttext-Länge:" << m_fullText.length(); // qDebug() << "\n=== Gesammelte Text-Fragmente ==="; // qDebug() << "Anzahl Fragmente:" << m_fragments.size(); @@ -91,19 +97,14 @@ class DOMTextManager } } - // Findet die Text-Position basierend auf Pixel-Koordinaten TextPosition getPositionAtCoordinates( int x, int y ) { TextPosition result; TextFragment* bestMatch = nullptr; int smallestArea = INT_MAX; - // qDebug() << "\n>>> Suche Element bei:" << x << y; - - // Suche das kleinste Element, das den Punkt enthält (spezifischstes Element) for ( auto& fragment : m_fragments ) { - // Nur Leaf-Nodes und Elemente mit gültiger Position prüfen if ( !fragment.is_leaf || !fragment.hasValidPosition() ) { continue; @@ -111,16 +112,11 @@ class DOMTextManager const auto& pos = fragment.pos; - // Prüfe ob Punkt im Element liegt if ( x >= pos.x && x <= pos.x + pos.width && y >= pos.y && y <= pos.y + pos.height ) { int area = pos.width * pos.height; - // qDebug() << " Kandidat:" << QString::fromStdString( fragment.text ).left( 20 ) << "| Box:" << pos.x << pos.y << pos.width << pos.height - // << "| Area:" << area; - - // Wähle das kleinste umschließende Element if ( area < smallestArea ) { smallestArea = area; @@ -134,7 +130,6 @@ class DOMTextManager result.element = bestMatch->element; result.element_pos = bestMatch->pos; - // Berechne Zeichen-Offset basierend auf x-Position const auto& pos = bestMatch->pos; if ( pos.width > 0 && bestMatch->text.length() > 0 ) { @@ -146,12 +141,6 @@ class DOMTextManager { result.char_offset = 0; } - - qDebug() << ">>> TREFFER:" << QString::fromStdString( bestMatch->text ).left( 30 ) << "| Offset:" << result.char_offset; - } - else - { - qDebug() << ">>> KEIN TREFFER!"; } return result; @@ -169,7 +158,6 @@ class DOMTextManager return selection; } - // Sicherstellen dass start vor end liegt TextPosition actualStart = start; TextPosition actualEnd = end; if ( end < start ) @@ -179,7 +167,6 @@ class DOMTextManager bool inSelection = false; - // Nur durch Leaf-Nodes iterieren for ( const auto& fragment : m_fragments ) { if ( !fragment.is_leaf || !fragment.hasValidPosition() ) @@ -214,6 +201,16 @@ class DOMTextManager selectedFragment.start_char_offset = startOffset; selectedFragment.end_char_offset = endOffset; + // WICHTIG: Position anpassen + litehtml::position adjustedPos = fragment.pos; + if ( fragment.text.length() > 0 && fragment.pos.width > 0 ) + { + float charWidth = static_cast( fragment.pos.width ) / static_cast( fragment.text.length() ); + adjustedPos.x = fragment.pos.x + static_cast( charWidth * startOffset ); + adjustedPos.width = static_cast( charWidth * ( endOffset - startOffset ) ); + } + selectedFragment.pos = adjustedPos; + selection.fragments.push_back( selectedFragment ); if ( isEndFragment ) @@ -223,28 +220,19 @@ class DOMTextManager } } - // qDebug() << "Selektion umfasst" << selection.fragments.size() << "Fragmente"; - return selection; } - const std::vector& getFragments() const { return m_fragments; } - std::vector findText( const std::string& search_term, bool case_sensitive = true ) { std::vector results; - if ( search_term.empty() || m_fullText.empty() ) { return results; } - // Normalisiere Text - std::string normalizedFullText = ( m_fullText ); - std::string normalizedSearchTerm = normalizeWhitespace( search_term ); - - std::string searchText = normalizedFullText; - std::string searchFor = normalizedSearchTerm; + std::string searchText = m_fullText; + std::string searchFor = search_term; if ( !case_sensitive ) { @@ -252,55 +240,34 @@ class DOMTextManager std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); } + qDebug() << "\n=== Suche ==="; + qDebug() << "Suchbegriff:" << QString::fromStdString( searchFor ); + qDebug() << "Durchsuche m_fullText (RAW):" << QString::fromStdString( searchText ).left( 100 ); + // Alle Vorkommen finden size_t pos = 0; while ( ( pos = searchText.find( searchFor, pos ) ) != std::string::npos ) { TextFindMatch result; - result.matched_text = normalizedFullText.substr( pos, searchFor.length() ); + result.matched_text = m_fullText.substr( pos, searchFor.length() ); // Jetzt korrekt! int searchStart = static_cast( pos ); int searchEnd = static_cast( pos + searchFor.length() ); - // Finde alle Fragmente in diesem Bereich + qDebug() << "Treffer bei:" << searchStart << "-" << searchEnd << "| Text:" << QString::fromStdString( result.matched_text ); + + // Finde Fragmente in diesem Bereich (jetzt mit korrekten Positionen) result.bounding_box = calculateBoundingBox( searchStart, searchEnd, result.fragments ); - // result.bounding_box = calculatePreciseBoundingBox( searchStart, searchEnd, result.fragments ); results.push_back( result ); pos += searchFor.length(); } - qDebug() << "Suche nach '" << QString::fromStdString( search_term ) << "': " << results.size() << " Treffer"; - + qDebug() << "Treffer gesamt:" << results.size() << "\n"; return results; } private: - std::string normalizeWhitespace( const std::string& text ) - { - std::string result; - bool lastWasSpace = false; - - for ( char c : text ) - { - if ( std::isspace( static_cast( c ) ) ) - { - if ( !lastWasSpace && !result.empty() ) - { - result += ' '; - lastWasSpace = true; - } - } - else - { - result += c; - lastWasSpace = false; - } - } - - return result; - } - void collectTextFragments( litehtml::element::ptr el ) { if (!el) return; @@ -319,23 +286,27 @@ class DOMTextManager { litehtml::position pos = el->get_placement(); - // Nur wenn Element eine gültige Position hat if ( pos.width > 0 && pos.height > 0 ) { TextFragment fragment; - fragment.text = normalizeWhitespace( text ); + fragment.text = text; // Original-Text fragment.element = el; fragment.pos = pos; fragment.start_char_offset = 0; - fragment.end_char_offset = fragment.text.length(); - fragment.global_start_offset = m_fullText.length(); + fragment.end_char_offset = text.length(); + fragment.global_start_offset = m_fullText.length(); // Start VOR dem Text fragment.is_leaf = true; - m_fullText += fragment.text + " "; - fragment.global_end_offset = m_fullText.length(); + // Text zum Volltext hinzufügen + m_fullText += text; + fragment.global_end_offset = m_fullText.length(); // Ende NACH dem Text m_fragments.push_back( fragment ); + // WICHTIG: Leerzeichen NACH dem Speichern des Fragments hinzufügen + // So zeigt global_end_offset auf das Ende des Textes, nicht auf das Leerzeichen + // m_fullText += " "; + // qDebug() << "Leaf-Fragment:" << QString::fromStdString( fragment.text ).left( 30 ) << "| Pos:" << pos.x << pos.y << "| Size:" << pos.width // << "x" << pos.height; } @@ -353,11 +324,11 @@ class DOMTextManager litehtml::position boundingBox = { 0, 0, 0, 0 }; bool firstFragment = true; - // qDebug() << "\n=== Berechne Bounding Box ==="; - // qDebug() << "Suchbereich: global offset" << searchStart << "bis" << searchEnd; - // qDebug() << "Gesuchter Text:" - // << QString::fromStdString( - // m_fullText.substr( searchStart, std::min( (size_t)( searchEnd - searchStart ), m_fullText.length() - searchStart ) ) ); + qDebug() << "\n=== Berechne BBox ==="; + qDebug() << "Suchbereich im m_fullText:" << searchStart << "-" << searchEnd; + qDebug() << "Gesuchter Text:" + << QString::fromStdString( m_fullText.substr( std::min( (size_t)searchStart, m_fullText.length() ), + std::min( (size_t)( searchEnd - searchStart ), m_fullText.length() - searchStart ) ) ); for ( const auto& fragment : m_fragments ) { @@ -366,62 +337,69 @@ class DOMTextManager continue; } + // qDebug() << "Prüfe Fragment:" << QString::fromStdString( fragment.text ).left( 20 ) << "| Global:" << fragment.global_start_offset << "-" + // << fragment.global_end_offset; + // Überlappung zwischen Suchbereich und Fragment + // WICHTIG: Fragment geht von global_start_offset bis global_end_offset + // Das Leerzeichen danach ist NICHT Teil des Fragments! int overlapStart = std::max( searchStart, fragment.global_start_offset ); int overlapEnd = std::min( searchEnd, fragment.global_end_offset ); if ( overlapStart < overlapEnd ) { - // Kopie des Fragments erstellen + qDebug() << " → Überlappung:" << overlapStart << "-" << overlapEnd; + TextFragment matchedFragment = fragment; // Lokale Offsets im Fragment-Text berechnen int localStart = overlapStart - fragment.global_start_offset; int localEnd = overlapEnd - fragment.global_start_offset; - // WICHTIG: Bounds checking - int maxLen = static_cast( fragment.text.length() ); - localStart = std::clamp( localStart, 0, maxLen ); - localEnd = std::clamp( localEnd, 0, maxLen ); + // Bounds checking + int textLen = static_cast( fragment.text.length() ); + localStart = std::clamp( localStart, 0, textLen ); + localEnd = std::clamp( localEnd, 0, textLen ); if ( localEnd <= localStart ) { - qDebug() << "Warnung: Ungültige Offsets für Fragment"; + qDebug() << " → FEHLER: Ungültige lokale Offsets!"; continue; } matchedFragment.start_char_offset = localStart; matchedFragment.end_char_offset = localEnd; - // WICHTIG: Position des matched Fragments anpassen + qDebug() << " → Lokale Offsets:" << localStart << "-" << localEnd + << "| Text:" << QString::fromStdString( fragment.text.substr( localStart, localEnd - localStart ) ); + + // Position für diesen Teil des Fragments berechnen litehtml::position adjustedPos = fragment.pos; - if ( maxLen > 0 && fragment.pos.width > 0 ) + if ( textLen > 0 && fragment.pos.width > 0 ) { - float charWidth = static_cast( fragment.pos.width ) / static_cast( maxLen ); + float charWidth = static_cast( fragment.pos.width ) / static_cast( textLen ); - // X-Position anpassen basierend auf localStart + // X-Position anpassen adjustedPos.x = fragment.pos.x + static_cast( charWidth * localStart ); - // Breite anpassen basierend auf Anzahl der Zeichen + // Breite anpassen adjustedPos.width = static_cast( charWidth * ( localEnd - localStart ) ); + + qDebug() << " → Angepasste Position:" << adjustedPos.x << adjustedPos.y << "| Größe:" << adjustedPos.width << "x" << adjustedPos.height + << "| CharWidth:" << charWidth; } - // Speichere die angepasste Position IM Fragment + // Speichere angepasste Position im Fragment matchedFragment.pos = adjustedPos; - matchedFragments.push_back( matchedFragment ); - // qDebug() << "Fragment gefunden:" << QString::fromStdString( fragment.text.substr( localStart, localEnd - localStart ) ) - // << "| Local:" << localStart << "-" << localEnd << "| Pos:" << adjustedPos.x << adjustedPos.y << "| Size:" << adjustedPos.width << - // "x" - // << adjustedPos.height; - - // Bounding Box erweitern + // Bounding Box erweitern/initialisieren if ( firstFragment ) { boundingBox = adjustedPos; firstFragment = false; + qDebug() << " → Initiale BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; } else { @@ -434,13 +412,15 @@ class DOMTextManager boundingBox.y = minY; boundingBox.width = maxX - minX; boundingBox.height = maxY - minY; + + qDebug() << " → Erweiterte BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; } } } - // qDebug() << "Finale Bounding Box:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; - // qDebug() << "=========================\n"; - + qDebug() << "Finale BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; + qDebug() << "Anzahl Fragmente:" << matchedFragments.size(); + qDebug() << "===================\n"; return boundingBox; } }; diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 08764d2..c0301e6 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -999,11 +999,10 @@ void container_qt::mouseMoveEvent( QMouseEvent* e ) } if ( m_isSelecting && m_selectionStart.isValid() ) { - DOMTextManager::TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.client.x(), mousePos.client.y() ); + DOMTextManager::TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.html.x(), mousePos.html.y() ); if ( currentPos.isValid() ) { - qDebug() << "selection is valid"; m_currentSelection = m_textManager.getSelectionBetween( m_selectionStart, currentPos ); viewport()->update(); } @@ -1020,14 +1019,13 @@ void container_qt::mouseReleaseEvent( QMouseEvent* e ) { litehtml::position::vector redraw_boxes; const auto mousePos = convertMousePos( e ); - if ( mDocument->on_lbutton_up( mousePos.html.x(), mousePos.html.y(), mousePos.client.x(), mousePos.client.y(), redraw_boxes ) ) + if ( mDocument->on_lbutton_up( mousePos.html.x(), mousePos.html.y(), mousePos.html.x(), mousePos.html.y(), redraw_boxes ) ) { // something changed, redraw of boxes required; e->accept(); } if ( e->button() == Qt::LeftButton ) { - qDebug() << "selection finished"; m_isSelecting = false; if ( !m_currentSelection.isEmpty() ) @@ -1061,8 +1059,8 @@ void container_qt::mousePressEvent( QMouseEvent* e ) } m_isSelecting = true; - m_selectionStart = m_textManager.getPositionAtCoordinates( mousePos.client.x(), mousePos.client.y() ); - qDebug() << "selection started with" << QPoint( m_selectionStart.element_pos.x, m_selectionStart.element_pos.y ); + m_selectionStart = m_textManager.getPositionAtCoordinates( mousePos.html.x(), mousePos.html.y() ); + // qDebug() << "selection started with" << QPoint( m_selectionStart.element_pos.x, m_selectionStart.element_pos.y ); m_currentSelection.clear(); e->accept(); update(); @@ -1477,11 +1475,11 @@ QString container_qt::selectedText() const QString::fromStdString( fragment.text.substr( fragment.start_char_offset, fragment.end_char_offset - fragment.start_char_offset ) ); result += fragmentText; - // Leerzeichen zwischen Fragmenten hinzufügen (außer beim letzten) - if ( i < m_currentSelection.fragments.size() - 1 ) - { - result += " "; - } + // // Leerzeichen zwischen Fragmenten hinzufügen (außer beim letzten) + // if ( i < m_currentSelection.fragments.size() - 1 ) + // { + // result += " "; + // } } } @@ -1519,9 +1517,6 @@ void container_qt::keyPressEvent( QKeyEvent* event ) void container_qt::drawSelection( QPainter& painter ) { - qDebug() << "container_qt::drawSelection"; - // Farben für Selektion (Windows-Style) - painter.save(); painter.setPen( Qt::NoPen ); painter.setBrush( mSelectionColor ); From b8d80a7f0e4fc1d9d1caa5558d597bb8384b4a65 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 30 Oct 2025 08:21:02 +0100 Subject: [PATCH 05/15] multi element text without spaces now --- test/library/test_find.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/library/test_find.cpp b/test/library/test_find.cpp index 7fb528a..d571e37 100644 --- a/test/library/test_find.cpp +++ b/test/library/test_find.cpp @@ -74,7 +74,7 @@ void FindTest::test_find_phrase_multi_element() auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->findText( QStringLiteral( "schließt ab. Absatz 48" ) ); + auto found = browser->findText( QStringLiteral( "schließt ab.Absatz 48" ) ); qApp->processEvents(); QCOMPARE( found, 1 ); } From a2b6e70579cb5c73626064caf9372ec47da19606 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 30 Oct 2025 12:47:15 +0100 Subject: [PATCH 06/15] context sensitiv selection text handling --- src/CMakeLists.txt | 3 +- src/DOMTextManager.h | 426 ------------------------------------ src/TextManager.cpp | 505 +++++++++++++++++++++++++++++++++++++++++++ src/TextManager.h | 93 ++++++++ src/container_qt.cpp | 38 +--- src/container_qt.h | 24 +- 6 files changed, 617 insertions(+), 472 deletions(-) delete mode 100644 src/DOMTextManager.h create mode 100644 src/TextManager.cpp create mode 100644 src/TextManager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e0d533..18ec27d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,7 +22,8 @@ target_sources(QLiteHtmlBrowser QLiteHtmlBrowser.cpp QLiteHtmlBrowserImpl.cpp QLiteHtmlBrowserImpl.h - DOMTextManager.h + TextManager.h + TextManager.cpp container_qt.cpp container_qt.h browserdefinitions.h diff --git a/src/DOMTextManager.h b/src/DOMTextManager.h deleted file mode 100644 index 6785939..0000000 --- a/src/DOMTextManager.h +++ /dev/null @@ -1,426 +0,0 @@ -#pragma once - -#include -#include - -class DOMTextManager -{ -public: - struct TextFragment - { - std::string text; - litehtml::element::ptr element; - litehtml::position pos; - int start_char_offset; // Offset in element text - int end_char_offset; // end Offset in element text - int global_start_offset; - int global_end_offset; - bool is_leaf; - - bool hasValidPosition() const { return pos.width > 0 && pos.height > 0; } - }; - - struct TextFindMatch - { - std::string matched_text; - std::vector fragments; - litehtml::position bounding_box; - - bool isEmpty() const { return fragments.empty(); } - }; - - struct TextPosition - { - litehtml::element::ptr element; - int char_offset = 0; - litehtml::position element_pos; - - bool operator<( const TextPosition& other ) const - { - if ( element_pos.y != other.element_pos.y ) - return element_pos.y < other.element_pos.y; - if ( element_pos.x != other.element_pos.x ) - return element_pos.x < other.element_pos.x; - return char_offset < other.char_offset; - } - bool isValid() const { return element && element_pos.width > 0 && element_pos.height > 0; } - }; - - struct SelectionRange - { - TextPosition start; - TextPosition end; - std::vector fragments; // Alle Textfragmente in der Selektion - - bool isEmpty() const { return !start.isValid() || !end.isValid(); } - - void clear() - { - start = TextPosition(); - end = TextPosition(); - fragments.clear(); - } - }; - -private: - std::vector m_fragments; - std::string m_fullText; - -public: - void buildFromDocument( litehtml::document::ptr doc ) - { - m_fragments.clear(); - m_fullText.clear(); - - if ( doc && doc->root() ) - { - collectTextFragments( doc->root() ); - if ( !m_fullText.empty() && m_fullText.back() == ' ' ) - { - m_fullText.pop_back(); - } - - // qDebug() << "\n=== DOM Text Manager initialisiert ==="; - // qDebug() << "Fragmente gesammelt:" << m_fragments.size(); - // qDebug() << "Gesamttext-Länge:" << m_fullText.length(); - - // qDebug() << "\n=== Gesammelte Text-Fragmente ==="; - // qDebug() << "Anzahl Fragmente:" << m_fragments.size(); - // for ( size_t i = 0; i < m_fragments.size(); ++i ) - // { - // const auto& frag = m_fragments[i]; - // qDebug() << "Fragment" << i << ":" - // << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y - // << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); - // } - // qDebug() << "================================\n"; - } - } - - TextPosition getPositionAtCoordinates( int x, int y ) - { - TextPosition result; - TextFragment* bestMatch = nullptr; - int smallestArea = INT_MAX; - - for ( auto& fragment : m_fragments ) - { - if ( !fragment.is_leaf || !fragment.hasValidPosition() ) - { - continue; - } - - const auto& pos = fragment.pos; - - if ( x >= pos.x && x <= pos.x + pos.width && y >= pos.y && y <= pos.y + pos.height ) - { - - int area = pos.width * pos.height; - - if ( area < smallestArea ) - { - smallestArea = area; - bestMatch = &fragment; - } - } - } - - if ( bestMatch ) - { - result.element = bestMatch->element; - result.element_pos = bestMatch->pos; - - const auto& pos = bestMatch->pos; - if ( pos.width > 0 && bestMatch->text.length() > 0 ) - { - float relativeX = static_cast( x - pos.x ) / pos.width; - result.char_offset = static_cast( relativeX * bestMatch->text.length() ); - result.char_offset = std::max( 0, std::min( result.char_offset, static_cast( bestMatch->text.length() ) ) ); - } - else - { - result.char_offset = 0; - } - } - - return result; - } - - // Extrahiert Text und Fragmente zwischen zwei Positionen - SelectionRange getSelectionBetween( const TextPosition& start, const TextPosition& end ) - { - SelectionRange selection; - selection.start = start; - selection.end = end; - - if ( !start.isValid() || !end.isValid() ) - { - return selection; - } - - TextPosition actualStart = start; - TextPosition actualEnd = end; - if ( end < start ) - { - std::swap( actualStart, actualEnd ); - } - - bool inSelection = false; - - for ( const auto& fragment : m_fragments ) - { - if ( !fragment.is_leaf || !fragment.hasValidPosition() ) - { - continue; - } - - bool isStartFragment = ( fragment.element == actualStart.element ); - bool isEndFragment = ( fragment.element == actualEnd.element ); - - if ( isStartFragment ) - { - inSelection = true; - } - - if ( inSelection ) - { - TextFragment selectedFragment = fragment; - int startOffset = 0; - int endOffset = fragment.text.length(); - - if ( isStartFragment ) - { - startOffset = actualStart.char_offset; - } - - if ( isEndFragment ) - { - endOffset = actualEnd.char_offset; - } - - selectedFragment.start_char_offset = startOffset; - selectedFragment.end_char_offset = endOffset; - - // WICHTIG: Position anpassen - litehtml::position adjustedPos = fragment.pos; - if ( fragment.text.length() > 0 && fragment.pos.width > 0 ) - { - float charWidth = static_cast( fragment.pos.width ) / static_cast( fragment.text.length() ); - adjustedPos.x = fragment.pos.x + static_cast( charWidth * startOffset ); - adjustedPos.width = static_cast( charWidth * ( endOffset - startOffset ) ); - } - selectedFragment.pos = adjustedPos; - - selection.fragments.push_back( selectedFragment ); - - if ( isEndFragment ) - { - break; - } - } - } - - return selection; - } - - std::vector findText( const std::string& search_term, bool case_sensitive = true ) - { - std::vector results; - if ( search_term.empty() || m_fullText.empty() ) - { - return results; - } - - std::string searchText = m_fullText; - std::string searchFor = search_term; - - if ( !case_sensitive ) - { - std::transform( searchText.begin(), searchText.end(), searchText.begin(), ::tolower ); - std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); - } - - qDebug() << "\n=== Suche ==="; - qDebug() << "Suchbegriff:" << QString::fromStdString( searchFor ); - qDebug() << "Durchsuche m_fullText (RAW):" << QString::fromStdString( searchText ).left( 100 ); - - // Alle Vorkommen finden - size_t pos = 0; - while ( ( pos = searchText.find( searchFor, pos ) ) != std::string::npos ) - { - TextFindMatch result; - result.matched_text = m_fullText.substr( pos, searchFor.length() ); // Jetzt korrekt! - - int searchStart = static_cast( pos ); - int searchEnd = static_cast( pos + searchFor.length() ); - - qDebug() << "Treffer bei:" << searchStart << "-" << searchEnd << "| Text:" << QString::fromStdString( result.matched_text ); - - // Finde Fragmente in diesem Bereich (jetzt mit korrekten Positionen) - result.bounding_box = calculateBoundingBox( searchStart, searchEnd, result.fragments ); - - results.push_back( result ); - pos += searchFor.length(); - } - - qDebug() << "Treffer gesamt:" << results.size() << "\n"; - return results; - } - -private: - void collectTextFragments( litehtml::element::ptr el ) - { - if (!el) return; - - // Prüfe ob Element Kinder hat - bool hasChildren = !el->children().empty(); - - // Text aus dem Element holen - std::string text; - el->get_text( text ); - - // NUR Leaf-Nodes (Elemente ohne Kinder) mit Text speichern - // Dadurch vermeiden wir, dass Parent-Elemente den gesamten - // aggregierten Text ihrer Kinder zurückgeben - if ( !hasChildren && !text.empty() ) - { - litehtml::position pos = el->get_placement(); - - if ( pos.width > 0 && pos.height > 0 ) - { - TextFragment fragment; - fragment.text = text; // Original-Text - fragment.element = el; - fragment.pos = pos; - fragment.start_char_offset = 0; - fragment.end_char_offset = text.length(); - fragment.global_start_offset = m_fullText.length(); // Start VOR dem Text - fragment.is_leaf = true; - - // Text zum Volltext hinzufügen - m_fullText += text; - fragment.global_end_offset = m_fullText.length(); // Ende NACH dem Text - - m_fragments.push_back( fragment ); - - // WICHTIG: Leerzeichen NACH dem Speichern des Fragments hinzufügen - // So zeigt global_end_offset auf das Ende des Textes, nicht auf das Leerzeichen - // m_fullText += " "; - - // qDebug() << "Leaf-Fragment:" << QString::fromStdString( fragment.text ).left( 30 ) << "| Pos:" << pos.x << pos.y << "| Size:" << pos.width - // << "x" << pos.height; - } - } - - // Rekursiv durch alle Kindelemente - for ( auto it = el->children().begin(); it != el->children().end(); ++it ) - { - collectTextFragments( *it ); - } - } - - litehtml::position calculateBoundingBox( int searchStart, int searchEnd, std::vector& matchedFragments ) - { - litehtml::position boundingBox = { 0, 0, 0, 0 }; - bool firstFragment = true; - - qDebug() << "\n=== Berechne BBox ==="; - qDebug() << "Suchbereich im m_fullText:" << searchStart << "-" << searchEnd; - qDebug() << "Gesuchter Text:" - << QString::fromStdString( m_fullText.substr( std::min( (size_t)searchStart, m_fullText.length() ), - std::min( (size_t)( searchEnd - searchStart ), m_fullText.length() - searchStart ) ) ); - - for ( const auto& fragment : m_fragments ) - { - if ( !fragment.is_leaf || !fragment.hasValidPosition() ) - { - continue; - } - - // qDebug() << "Prüfe Fragment:" << QString::fromStdString( fragment.text ).left( 20 ) << "| Global:" << fragment.global_start_offset << "-" - // << fragment.global_end_offset; - - // Überlappung zwischen Suchbereich und Fragment - // WICHTIG: Fragment geht von global_start_offset bis global_end_offset - // Das Leerzeichen danach ist NICHT Teil des Fragments! - int overlapStart = std::max( searchStart, fragment.global_start_offset ); - int overlapEnd = std::min( searchEnd, fragment.global_end_offset ); - - if ( overlapStart < overlapEnd ) - { - qDebug() << " → Überlappung:" << overlapStart << "-" << overlapEnd; - - TextFragment matchedFragment = fragment; - - // Lokale Offsets im Fragment-Text berechnen - int localStart = overlapStart - fragment.global_start_offset; - int localEnd = overlapEnd - fragment.global_start_offset; - - // Bounds checking - int textLen = static_cast( fragment.text.length() ); - localStart = std::clamp( localStart, 0, textLen ); - localEnd = std::clamp( localEnd, 0, textLen ); - - if ( localEnd <= localStart ) - { - qDebug() << " → FEHLER: Ungültige lokale Offsets!"; - continue; - } - - matchedFragment.start_char_offset = localStart; - matchedFragment.end_char_offset = localEnd; - - qDebug() << " → Lokale Offsets:" << localStart << "-" << localEnd - << "| Text:" << QString::fromStdString( fragment.text.substr( localStart, localEnd - localStart ) ); - - // Position für diesen Teil des Fragments berechnen - litehtml::position adjustedPos = fragment.pos; - - if ( textLen > 0 && fragment.pos.width > 0 ) - { - float charWidth = static_cast( fragment.pos.width ) / static_cast( textLen ); - - // X-Position anpassen - adjustedPos.x = fragment.pos.x + static_cast( charWidth * localStart ); - - // Breite anpassen - adjustedPos.width = static_cast( charWidth * ( localEnd - localStart ) ); - - qDebug() << " → Angepasste Position:" << adjustedPos.x << adjustedPos.y << "| Größe:" << adjustedPos.width << "x" << adjustedPos.height - << "| CharWidth:" << charWidth; - } - - // Speichere angepasste Position im Fragment - matchedFragment.pos = adjustedPos; - matchedFragments.push_back( matchedFragment ); - - // Bounding Box erweitern/initialisieren - if ( firstFragment ) - { - boundingBox = adjustedPos; - firstFragment = false; - qDebug() << " → Initiale BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; - } - else - { - int minX = std::min( boundingBox.x, adjustedPos.x ); - int minY = std::min( boundingBox.y, adjustedPos.y ); - int maxX = std::max( boundingBox.x + boundingBox.width, adjustedPos.x + adjustedPos.width ); - int maxY = std::max( boundingBox.y + boundingBox.height, adjustedPos.y + adjustedPos.height ); - - boundingBox.x = minX; - boundingBox.y = minY; - boundingBox.width = maxX - minX; - boundingBox.height = maxY - minY; - - qDebug() << " → Erweiterte BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; - } - } - } - - qDebug() << "Finale BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; - qDebug() << "Anzahl Fragmente:" << matchedFragments.size(); - qDebug() << "===================\n"; - return boundingBox; - } -}; diff --git a/src/TextManager.cpp b/src/TextManager.cpp new file mode 100644 index 0000000..3274dd9 --- /dev/null +++ b/src/TextManager.cpp @@ -0,0 +1,505 @@ +#include "TextManager.h" +#include +#include + +void TextManager::buildFromDocument( litehtml::document::ptr doc ) +{ + m_fragments.clear(); + m_fullText.clear(); + + if ( doc && doc->root() ) + { + collectTextFragments( doc->root() ); + if ( !m_fullText.empty() && m_fullText.back() == ' ' ) + { + m_fullText.pop_back(); + } + + // qDebug() << "\n=== DOM Text Manager initialisiert ==="; + // qDebug() << "Fragmente gesammelt:" << m_fragments.size(); + // qDebug() << "Gesamttext-Länge:" << m_fullText.length(); + + // qDebug() << "\n=== Gesammelte Text-Fragmente ==="; + // qDebug() << "Anzahl Fragmente:" << m_fragments.size(); + // for ( size_t i = 0; i < m_fragments.size(); ++i ) + // { + // const auto& frag = m_fragments[i]; + // qDebug() << "Fragment" << i << ":" + // << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y + // << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); + // } + // qDebug() << "================================\n"; + } +} + +TextManager::TextPosition TextManager::getPositionAtCoordinates( int x, int y ) +{ + TextPosition result; + TextFragment* bestMatch = nullptr; + int smallestArea = INT_MAX; + + for ( auto& fragment : m_fragments ) + { + if ( !fragment.is_leaf || !fragment.hasValidPosition() ) + { + continue; + } + + const auto& pos = fragment.pos; + + if ( x >= pos.x && x <= pos.x + pos.width && y >= pos.y && y <= pos.y + pos.height ) + { + + int area = pos.width * pos.height; + + if ( area < smallestArea ) + { + smallestArea = area; + bestMatch = &fragment; + } + } + } + + if ( bestMatch ) + { + result.element = bestMatch->element; + result.element_pos = bestMatch->pos; + + const auto& pos = bestMatch->pos; + if ( pos.width > 0 && bestMatch->text.length() > 0 ) + { + float relativeX = static_cast( x - pos.x ) / pos.width; + result.char_offset = static_cast( relativeX * bestMatch->text.length() ); + result.char_offset = std::max( 0, std::min( result.char_offset, static_cast( bestMatch->text.length() ) ) ); + } + else + { + result.char_offset = 0; + } + } + + return result; +} + +// Extrahiert Text und Fragmente zwischen zwei Positionen +TextManager::SelectionRange TextManager::getSelectionBetween( const TextManager::TextPosition& start, const TextManager::TextPosition& end ) +{ + SelectionRange selection; + selection.start = start; + selection.end = end; + + if ( !start.isValid() || !end.isValid() ) + { + return selection; + } + + TextPosition actualStart = start; + TextPosition actualEnd = end; + if ( end < start ) + { + std::swap( actualStart, actualEnd ); + } + + bool inSelection = false; + + for ( const auto& fragment : m_fragments ) + { + if ( !fragment.is_leaf || !fragment.hasValidPosition() ) + { + continue; + } + + bool isStartFragment = ( fragment.element == actualStart.element ); + bool isEndFragment = ( fragment.element == actualEnd.element ); + + if ( isStartFragment ) + { + inSelection = true; + } + + if ( inSelection ) + { + TextFragment selectedFragment = fragment; + int startOffset = 0; + int endOffset = fragment.text.length(); + + if ( isStartFragment ) + { + startOffset = actualStart.char_offset; + } + + if ( isEndFragment ) + { + endOffset = actualEnd.char_offset; + } + + selectedFragment.start_char_offset = startOffset; + selectedFragment.end_char_offset = endOffset; + + // WICHTIG: Position anpassen + litehtml::position adjustedPos = fragment.pos; + if ( fragment.text.length() > 0 && fragment.pos.width > 0 ) + { + float charWidth = static_cast( fragment.pos.width ) / static_cast( fragment.text.length() ); + adjustedPos.x = fragment.pos.x + static_cast( charWidth * startOffset ); + adjustedPos.width = static_cast( charWidth * ( endOffset - startOffset ) ); + } + selectedFragment.pos = adjustedPos; + + selection.fragments.push_back( selectedFragment ); + + if ( isEndFragment ) + { + break; + } + } + } + + return selection; +} + +std::vector TextManager::findText( const std::string& search_term, bool case_sensitive ) +{ + std::vector results; + if ( search_term.empty() || m_fullText.empty() ) + { + return results; + } + + std::string searchText = m_fullText; + std::string searchFor = search_term; + + if ( !case_sensitive ) + { + std::transform( searchText.begin(), searchText.end(), searchText.begin(), ::tolower ); + std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); + } + + qDebug() << "\n=== Suche ==="; + qDebug() << "Suchbegriff:" << QString::fromStdString( searchFor ); + qDebug() << "Durchsuche m_fullText (RAW):" << QString::fromStdString( searchText ).left( 100 ); + + // Alle Vorkommen finden + size_t pos = 0; + while ( ( pos = searchText.find( searchFor, pos ) ) != std::string::npos ) + { + TextFindMatch result; + result.matched_text = m_fullText.substr( pos, searchFor.length() ); // Jetzt korrekt! + + int searchStart = static_cast( pos ); + int searchEnd = static_cast( pos + searchFor.length() ); + + qDebug() << "Treffer bei:" << searchStart << "-" << searchEnd << "| Text:" << QString::fromStdString( result.matched_text ); + + // Finde Fragmente in diesem Bereich (jetzt mit korrekten Positionen) + result.bounding_box = calculateBoundingBox( searchStart, searchEnd, result.fragments ); + + results.push_back( result ); + pos += searchFor.length(); + } + + qDebug() << "Treffer gesamt:" << results.size() << "\n"; + return results; +} + +void TextManager::collectTextFragments( litehtml::element::ptr el ) +{ + if ( !el ) + return; + + // Prüfe ob Element Kinder hat + bool hasChildren = !el->children().empty(); + + // Text aus dem Element holen + std::string text; + el->get_text( text ); + + // NUR Leaf-Nodes (Elemente ohne Kinder) mit Text speichern + // Dadurch vermeiden wir, dass Parent-Elemente den gesamten + // aggregierten Text ihrer Kinder zurückgeben + if ( !hasChildren && !text.empty() ) + { + litehtml::position pos = el->get_placement(); + + if ( pos.width > 0 && pos.height > 0 ) + { + TextFragment fragment; + fragment.text = text; // Original-Text + fragment.parent_element = el->parent(); + fragment.element = el; + fragment.pos = pos; + fragment.start_char_offset = 0; + fragment.end_char_offset = text.length(); + fragment.global_start_offset = m_fullText.length(); // Start VOR dem Text + fragment.is_leaf = true; + + // Text zum Volltext hinzufügen + m_fullText += text; + fragment.global_end_offset = m_fullText.length(); // Ende NACH dem Text + fragment.element_tag = getElementTag( el ); + fragment.is_block_element = isBlockLevelElement( fragment.element_tag ); + + m_fragments.push_back( fragment ); + + // WICHTIG: Leerzeichen NACH dem Speichern des Fragments hinzufügen + // So zeigt global_end_offset auf das Ende des Textes, nicht auf das Leerzeichen + // m_fullText += " "; + + // qDebug() << "Leaf-Fragment:" << QString::fromStdString( fragment.text ).left( 30 ) << "| Pos:" << pos.x << pos.y << "| Size:" << pos.width + // << "x" << pos.height; + } + } + + // Rekursiv durch alle Kindelemente + for ( auto it = el->children().begin(); it != el->children().end(); ++it ) + { + collectTextFragments( *it ); + } +} + +litehtml::position TextManager::calculateBoundingBox( int searchStart, int searchEnd, std::vector& matchedFragments ) +{ + litehtml::position boundingBox = { 0, 0, 0, 0 }; + bool firstFragment = true; + + qDebug() << "\n=== Berechne BBox ==="; + qDebug() << "Suchbereich im m_fullText:" << searchStart << "-" << searchEnd; + qDebug() << "Gesuchter Text:" + << QString::fromStdString( m_fullText.substr( std::min( (size_t)searchStart, m_fullText.length() ), + std::min( (size_t)( searchEnd - searchStart ), m_fullText.length() - searchStart ) ) ); + + for ( const auto& fragment : m_fragments ) + { + if ( !fragment.is_leaf || !fragment.hasValidPosition() ) + { + continue; + } + + // qDebug() << "Prüfe Fragment:" << QString::fromStdString( fragment.text ).left( 20 ) << "| Global:" << fragment.global_start_offset << "-" + // << fragment.global_end_offset; + + // Überlappung zwischen Suchbereich und Fragment + // WICHTIG: Fragment geht von global_start_offset bis global_end_offset + // Das Leerzeichen danach ist NICHT Teil des Fragments! + int overlapStart = std::max( searchStart, fragment.global_start_offset ); + int overlapEnd = std::min( searchEnd, fragment.global_end_offset ); + + if ( overlapStart < overlapEnd ) + { + qDebug() << " → Überlappung:" << overlapStart << "-" << overlapEnd; + + TextFragment matchedFragment = fragment; + + // Lokale Offsets im Fragment-Text berechnen + int localStart = overlapStart - fragment.global_start_offset; + int localEnd = overlapEnd - fragment.global_start_offset; + + // Bounds checking + int textLen = static_cast( fragment.text.length() ); + localStart = std::clamp( localStart, 0, textLen ); + localEnd = std::clamp( localEnd, 0, textLen ); + + if ( localEnd <= localStart ) + { + qDebug() << " → FEHLER: Ungültige lokale Offsets!"; + continue; + } + + matchedFragment.start_char_offset = localStart; + matchedFragment.end_char_offset = localEnd; + + qDebug() << " → Lokale Offsets:" << localStart << "-" << localEnd + << "| Text:" << QString::fromStdString( fragment.text.substr( localStart, localEnd - localStart ) ); + + // Position für diesen Teil des Fragments berechnen + litehtml::position adjustedPos = fragment.pos; + + if ( textLen > 0 && fragment.pos.width > 0 ) + { + float charWidth = static_cast( fragment.pos.width ) / static_cast( textLen ); + + // X-Position anpassen + adjustedPos.x = fragment.pos.x + static_cast( charWidth * localStart ); + + // Breite anpassen + adjustedPos.width = static_cast( charWidth * ( localEnd - localStart ) ); + + qDebug() << " → Angepasste Position:" << adjustedPos.x << adjustedPos.y << "| Größe:" << adjustedPos.width << "x" << adjustedPos.height + << "| CharWidth:" << charWidth; + } + + // Speichere angepasste Position im Fragment + matchedFragment.pos = adjustedPos; + matchedFragments.push_back( matchedFragment ); + + // Bounding Box erweitern/initialisieren + if ( firstFragment ) + { + boundingBox = adjustedPos; + firstFragment = false; + qDebug() << " → Initiale BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; + } + else + { + int minX = std::min( boundingBox.x, adjustedPos.x ); + int minY = std::min( boundingBox.y, adjustedPos.y ); + int maxX = std::max( boundingBox.x + boundingBox.width, adjustedPos.x + adjustedPos.width ); + int maxY = std::max( boundingBox.y + boundingBox.height, adjustedPos.y + adjustedPos.height ); + + boundingBox.x = minX; + boundingBox.y = minY; + boundingBox.width = maxX - minX; + boundingBox.height = maxY - minY; + + qDebug() << " → Erweiterte BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; + } + } + } + + qDebug() << "Finale BBox:" << boundingBox.x << boundingBox.y << boundingBox.width << "x" << boundingBox.height; + qDebug() << "Anzahl Fragmente:" << matchedFragments.size(); + qDebug() << "===================\n"; + return boundingBox; +} + +// Ermittle HTML-Tag des Elements oder seiner Eltern +std::string TextManager::getElementTag( litehtml::element::ptr element ) const +{ + if ( !element ) + return ""; + + // Gehe zum Parent-Element, da Textknoten selbst kein Tag haben + auto parent = element->parent(); + if ( !parent ) + return ""; + + // Hole Tag-Name + const char* tag = parent->get_tagName(); + if ( tag ) + { + std::string tagName( tag ); + std::transform( tagName.begin(), tagName.end(), tagName.begin(), ::tolower ); + return tagName; + } + + return ""; +} + +// Prüfe ob Element ein Block-Level-Element ist +bool TextManager::isBlockLevelElement( const std::string& tag ) const +{ + // Liste der gängigen Block-Level-Elemente + static const std::vector blockElements = { + "address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", + "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main", + "nav", "noscript", "ol", "p", "pre", "section", "table", "tfoot", "ul", "video", + "br" // BR ist speziell: inline aber erzeugt Zeilenumbruch + }; + + return std::find( blockElements.begin(), blockElements.end(), tag ) != blockElements.end(); +} + +bool TextManager::shouldInsertNewline( const TextFragment& prev, const TextFragment& current ) const +{ + // WICHTIG: Prüfe ob die Fragmente das GLEICHE Parent-Element haben + if ( prev.parent_element == current.parent_element ) + { + // Gleicher Parent = kein Newline (z.B. mehrere Textknoten in einem

) + return false; + } + + // Unterschiedliche Parents: Prüfe ob eines davon ein Block-Element ist + if ( prev.is_block_element || current.is_block_element ) + { + return true; + } + + return false; +} + +std::string TextManager::selectedText( const SelectionRange& selection, TextFormatMode mode ) const +{ + if ( selection.isEmpty() ) + { + return {}; + } + + std::string result; + + if ( mode == TextFormatMode::Structured ) + { + for ( size_t i = 0; i < selection.fragments.size(); ++i ) + { + const auto& fragment = selection.fragments[i]; + + if ( fragment.end_char_offset > fragment.start_char_offset ) + { + std::string part = fragment.text.substr( fragment.start_char_offset, fragment.end_char_offset - fragment.start_char_offset ); + + if ( i > 0 ) + { + const auto& prevFragment = selection.fragments[i - 1]; + + // WICHTIG: Prüfe ob gleicher Parent + if ( prevFragment.parent_element != fragment.parent_element ) + { + // Unterschiedliches Parent-Element + if ( prevFragment.element_tag == "p" && fragment.element_tag == "p" ) + { + // Zwischen zwei

-Elementen: Doppelter Newline + result += "\n\n"; + } + else if ( prevFragment.is_block_element || fragment.is_block_element ) + { + // Anderes Block-Element: Einfacher Newline + result += "\n"; + } + else + { + if ( prevFragment.element_tag != fragment.element_tag ) + { + // Inline-Elemente mit unterschiedlichem Parent: Space + result += ' '; + } + } + } + else + { + // Gleiches Parent-Element: Nur Space result += ' '; + } + } + + result += part; + } + } + } + else // Plain mode + { + bool first = true; + for ( const auto& fragment : selection.fragments ) + { + if ( fragment.end_char_offset > fragment.start_char_offset ) + { + std::string part = fragment.text.substr( fragment.start_char_offset, fragment.end_char_offset - fragment.start_char_offset ); + + if ( !first ) + { + result += ' '; + } + result += part; + first = false; + } + } + } + + // Trimme Whitespace + auto start = result.find_first_not_of( " \t\n\r" ); + auto end = result.find_last_not_of( " \t\n\r" ); + + if ( start == std::string::npos ) + { + return ""; + } + + return result.substr( start, end - start + 1 ); +} diff --git a/src/TextManager.h b/src/TextManager.h new file mode 100644 index 0000000..bf7fb58 --- /dev/null +++ b/src/TextManager.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +class TextManager +{ +public: + struct TextFragment + { + std::string text; + litehtml::element::ptr element; + litehtml::position pos; + int start_char_offset; // Offset in element text + int end_char_offset; // end Offset in element text + int global_start_offset; + int global_end_offset; + bool is_leaf; + litehtml::element::ptr parent_element; + + std::string element_tag; // z.B. "p", "div", "span", "br" + bool is_block_element; // true für Block-Level-Elemente + + bool hasValidPosition() const { return pos.width > 0 && pos.height > 0; } + }; + + struct TextFindMatch + { + std::string matched_text; + std::vector fragments; + litehtml::position bounding_box; + + bool isEmpty() const { return fragments.empty(); } + }; + + struct TextPosition + { + litehtml::element::ptr element; + int char_offset = 0; + litehtml::position element_pos; + + bool operator<( const TextPosition& other ) const + { + if ( element_pos.y != other.element_pos.y ) + return element_pos.y < other.element_pos.y; + if ( element_pos.x != other.element_pos.x ) + return element_pos.x < other.element_pos.x; + return char_offset < other.char_offset; + } + bool isValid() const { return element && element_pos.width > 0 && element_pos.height > 0; } + }; + + struct SelectionRange + { + TextPosition start; + TextPosition end; + std::vector fragments; // Alle Textfragmente in der Selektion + + bool isEmpty() const { return !start.isValid() || !end.isValid(); } + + void clear() + { + start = TextPosition(); + end = TextPosition(); + fragments.clear(); + } + }; + + enum class TextFormatMode + { + Plain, // Nur Leerzeichen zwischen Fragmenten + Structured, // Kontextsensitiv: Berücksichtigt Block-Elemente + Html // Behält HTML-Struktur bei (für zukünftige Erweiterung) + }; + +private: + std::vector m_fragments; + std::string m_fullText; + +public: + void buildFromDocument( litehtml::document::ptr doc ); + TextPosition getPositionAtCoordinates( int x, int y ); + SelectionRange getSelectionBetween( const TextPosition& start, const TextPosition& end ); + std::vector findText( const std::string& search_term, bool case_sensitive = true ); + std::string selectedText( const SelectionRange& selection, TextFormatMode mode = TextFormatMode::Structured ) const; + +private: + void collectTextFragments( litehtml::element::ptr el ); + litehtml::position calculateBoundingBox( int searchStart, int searchEnd, std::vector& matchedFragments ); + bool isBlockLevelElement( const std::string& tag ) const; + std::string getElementTag( litehtml::element::ptr element ) const; + bool shouldInsertNewline( const TextFragment& prev, const TextFragment& current ) const; +}; diff --git a/src/container_qt.cpp b/src/container_qt.cpp index c0301e6..5d73be0 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -999,7 +999,7 @@ void container_qt::mouseMoveEvent( QMouseEvent* e ) } if ( m_isSelecting && m_selectionStart.isValid() ) { - DOMTextManager::TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.html.x(), mousePos.html.y() ); + TextManager::TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.html.x(), mousePos.html.y() ); if ( currentPos.isValid() ) { @@ -1359,7 +1359,7 @@ bool container_qt::find_previous_match() } // Aktuelles Suchergebnis mit Position holen -const DOMTextManager::TextFindMatch* container_qt::find_current_match() const +const TextManager::TextFindMatch* container_qt::find_current_match() const { if ( mFindCurrentMatchIndex >= 0 && mFindCurrentMatchIndex < static_cast( mFindMatches.size() ) ) { @@ -1382,7 +1382,7 @@ void container_qt::draw_highlights( litehtml::uint_ptr hdc ) } // draw highlighted text at position -void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const DOMTextManager::TextFindMatch& match ) +void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const TextManager::TextFindMatch& match ) { qDebug() << "Highlighting text '" << QString::fromStdString( match.matched_text ) << "' at position (" << pos.x << ", " << pos.y << ") with size " @@ -1424,7 +1424,7 @@ void container_qt::findPreviousMatch() } } -void container_qt::scroll_to_find_match( const DOMTextManager::TextFindMatch* match ) +void container_qt::scroll_to_find_match( const TextManager::TextFindMatch* match ) { if ( !match ) { @@ -1457,35 +1457,7 @@ QString container_qt::selectedText() const return QString(); } - QString result; - // for ( const auto& fragment : m_currentSelection.fragments ) - // { - // QString fragmentText = - // QString::fromStdString( fragment.text.substr( fragment.start_char_offset, fragment.end_char_offset - fragment.start_char_offset ) ); - // result += fragmentText; - // } - - for ( size_t i = 0; i < m_currentSelection.fragments.size(); ++i ) - { - const auto& fragment = m_currentSelection.fragments[i]; - - if ( fragment.end_char_offset > fragment.start_char_offset ) - { - QString fragmentText = - QString::fromStdString( fragment.text.substr( fragment.start_char_offset, fragment.end_char_offset - fragment.start_char_offset ) ); - result += fragmentText; - - // // Leerzeichen zwischen Fragmenten hinzufügen (außer beim letzten) - // if ( i < m_currentSelection.fragments.size() - 1 ) - // { - // result += " "; - // } - } - } - - return result; - - return result; + return QString::fromStdString( m_textManager.selectedText( m_currentSelection ) ); } // const DOMTextManager::SelectionRange& container_qt::getCurrentSelection() const diff --git a/src/container_qt.h b/src/container_qt.h index b753d24..1f1fbff 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -1,7 +1,7 @@ #pragma once #include "litehtml.h" -#include "DOMTextManager.h" +#include "TextManager.h" #include "browserdefinitions.h" #include @@ -130,21 +130,21 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co int find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); bool find_next_match(); bool find_previous_match(); - const DOMTextManager::TextFindMatch* find_current_match() const; - void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const DOMTextManager::TextFindMatch& match ); + const TextManager::TextFindMatch* find_current_match() const; + void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const TextManager::TextFindMatch& match ); void draw_highlights( litehtml::uint_ptr hdc ); void clear_highlights() { mFindMatches.clear(); } // std::string normalizeWhitespace( const std::string& text ); // void find_text_in_document( litehtml::document::ptr doc, // const std::string& search_term, - // std::vector& matches, + // std::vector& matches, // bool case_sensitive = true ); - // void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText - // ); litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, + // void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText + // ); litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, // int searchStart, // int searchEnd, - // std::vector& matchedFragments ); - void scroll_to_find_match( const DOMTextManager::TextFindMatch* ); + // std::vector& matchedFragments ); + void scroll_to_find_match( const TextManager::TextFindMatch* ); std::shared_ptr mDocument; QByteArray mDocumentSource; @@ -165,7 +165,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co QString mUserCSS; QStack mClipStack; litehtml::position mClip = {}; - std::vector mFindMatches = {}; + std::vector mFindMatches = {}; int mFindCurrentMatchIndex = -1; QColor mHighlightColor = QColor( 255, 255, 0, 30 ); QColor mSelectionColor = QColor( 0, 120, 215, 100 ); @@ -173,8 +173,8 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co // for selection - DOMTextManager m_textManager; - DOMTextManager::SelectionRange m_currentSelection; + TextManager m_textManager; + TextManager::SelectionRange m_currentSelection; bool m_isSelecting; - DOMTextManager::TextPosition m_selectionStart; + TextManager::TextPosition m_selectionStart; }; From 34fb0a4f1fde1879a0736dfe7841d0277a64e9b7 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 30 Oct 2025 16:18:18 +0100 Subject: [PATCH 07/15] improve update of selections during mouse handling --- src/TextManager.cpp | 20 ++++++++++---------- src/TextManager.h | 21 +++++++++++---------- src/container_qt.cpp | 19 +++++++++++++++---- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/TextManager.cpp b/src/TextManager.cpp index 3274dd9..c00e76d 100644 --- a/src/TextManager.cpp +++ b/src/TextManager.cpp @@ -19,16 +19,16 @@ void TextManager::buildFromDocument( litehtml::document::ptr doc ) // qDebug() << "Fragmente gesammelt:" << m_fragments.size(); // qDebug() << "Gesamttext-Länge:" << m_fullText.length(); - // qDebug() << "\n=== Gesammelte Text-Fragmente ==="; - // qDebug() << "Anzahl Fragmente:" << m_fragments.size(); - // for ( size_t i = 0; i < m_fragments.size(); ++i ) - // { - // const auto& frag = m_fragments[i]; - // qDebug() << "Fragment" << i << ":" - // << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y - // << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); - // } - // qDebug() << "================================\n"; + qDebug() << "\n=== Gesammelte Text-Fragmente ==="; + qDebug() << "Anzahl Fragmente:" << m_fragments.size(); + for ( size_t i = 0; i < m_fragments.size(); ++i ) + { + const auto& frag = m_fragments[i]; + qDebug() << "Fragment" << i << ":" + << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y + << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); + } + qDebug() << "================================\n"; } } diff --git a/src/TextManager.h b/src/TextManager.h index bf7fb58..2db1764 100644 --- a/src/TextManager.h +++ b/src/TextManager.h @@ -5,7 +5,7 @@ class TextManager { -public: +private: struct TextFragment { std::string text; @@ -24,15 +24,7 @@ class TextManager bool hasValidPosition() const { return pos.width > 0 && pos.height > 0; } }; - struct TextFindMatch - { - std::string matched_text; - std::vector fragments; - litehtml::position bounding_box; - - bool isEmpty() const { return fragments.empty(); } - }; - +public: struct TextPosition { litehtml::element::ptr element; @@ -50,6 +42,15 @@ class TextManager bool isValid() const { return element && element_pos.width > 0 && element_pos.height > 0; } }; + struct TextFindMatch + { + std::string matched_text; + std::vector fragments; + litehtml::position bounding_box; + + bool isEmpty() const { return fragments.empty(); } + }; + struct SelectionRange { TextPosition start; diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 5d73be0..5adc508 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -999,11 +999,12 @@ void container_qt::mouseMoveEvent( QMouseEvent* e ) } if ( m_isSelecting && m_selectionStart.isValid() ) { + e->accept(); TextManager::TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.html.x(), mousePos.html.y() ); - if ( currentPos.isValid() ) { m_currentSelection = m_textManager.getSelectionBetween( m_selectionStart, currentPos ); + emit selectionChanged(); viewport()->update(); } } @@ -1026,8 +1027,18 @@ void container_qt::mouseReleaseEvent( QMouseEvent* e ) } if ( e->button() == Qt::LeftButton ) { + e->accept(); m_isSelecting = false; + // update selection position from this mouse pos + TextManager::TextPosition currentPos = m_textManager.getPositionAtCoordinates( mousePos.html.x(), mousePos.html.y() ); + + if ( currentPos.isValid() ) + { + m_currentSelection = m_textManager.getSelectionBetween( m_selectionStart, currentPos ); + viewport()->update(); + } + if ( !m_currentSelection.isEmpty() ) { qDebug() << "selection got selected text with " << selectedText().count() << "fragments"; @@ -1057,13 +1068,13 @@ void container_qt::mousePressEvent( QMouseEvent* e ) { // something changed, redraw of boxes required; } + m_currentSelection.clear(); m_isSelecting = true; m_selectionStart = m_textManager.getPositionAtCoordinates( mousePos.html.x(), mousePos.html.y() ); // qDebug() << "selection started with" << QPoint( m_selectionStart.element_pos.x, m_selectionStart.element_pos.y ); - m_currentSelection.clear(); e->accept(); - update(); + viewport()->update(); } else e->ignore(); @@ -1468,7 +1479,7 @@ QString container_qt::selectedText() const void container_qt::clearSelection() { m_currentSelection.clear(); - update(); + viewport()->update(); } void container_qt::keyPressEvent( QKeyEvent* event ) From eb71f40cf74be05a5dabe5d1e47a33635c55ab54 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Fri, 31 Oct 2025 08:30:35 +0100 Subject: [PATCH 08/15] add test for find bounding boxes --- include/qlitehtmlbrowser/QLiteHtmlBrowser.h | 6 +- src/QLiteHtmlBrowserImpl.h | 4 + src/TextManager.cpp | 20 +- src/container_qt.cpp | 198 +------------------- src/container_qt.h | 23 +-- test/library/CMakeLists.txt | 6 +- test/library/test_find.cpp | 88 +++++++-- test/library/test_find.h | 3 + 8 files changed, 114 insertions(+), 234 deletions(-) diff --git a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h index 7eae884..6856833 100644 --- a/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +++ b/include/qlitehtmlbrowser/QLiteHtmlBrowser.h @@ -25,6 +25,11 @@ class QLITEHTMLBROWSER_EXPORT QLiteHtmlBrowser : public QWidget { Q_OBJECT +#ifdef UNIT_TEST + friend class FindTest; + friend class SelectionTest; +#endif + /// property to access the url that is currently shown in the browser Q_PROPERTY( QUrl source READ source WRITE setSource ) @@ -194,6 +199,5 @@ public Q_SLOTS: void selectionChanged(); protected: -private: QLiteHtmlBrowserImpl* mImpl = nullptr; }; diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index c97dac1..3bb6479 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -83,6 +83,10 @@ class QLiteHtmlBrowserImpl : public QWidget void scaleChanged(); void selectionChanged(); +#ifdef UNIT_TEST + container_qt* const container() const { return mContainer; } +#endif + private: class HistoryEntry { diff --git a/src/TextManager.cpp b/src/TextManager.cpp index c00e76d..3274dd9 100644 --- a/src/TextManager.cpp +++ b/src/TextManager.cpp @@ -19,16 +19,16 @@ void TextManager::buildFromDocument( litehtml::document::ptr doc ) // qDebug() << "Fragmente gesammelt:" << m_fragments.size(); // qDebug() << "Gesamttext-Länge:" << m_fullText.length(); - qDebug() << "\n=== Gesammelte Text-Fragmente ==="; - qDebug() << "Anzahl Fragmente:" << m_fragments.size(); - for ( size_t i = 0; i < m_fragments.size(); ++i ) - { - const auto& frag = m_fragments[i]; - qDebug() << "Fragment" << i << ":" - << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y - << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); - } - qDebug() << "================================\n"; + // qDebug() << "\n=== Gesammelte Text-Fragmente ==="; + // qDebug() << "Anzahl Fragmente:" << m_fragments.size(); + // for ( size_t i = 0; i < m_fragments.size(); ++i ) + // { + // const auto& frag = m_fragments[i]; + // qDebug() << "Fragment" << i << ":" + // << "Text:" << QString::fromStdString( frag.text ).left( 30 ) << "| Leaf:" << frag.is_leaf << "| Pos:" << frag.pos.x << frag.pos.y + // << "| Size:" << frag.pos.width << "x" << frag.pos.height << "| Valid:" << frag.hasValidPosition(); + // } + // qDebug() << "================================\n"; } } diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 5adc508..91e7d4a 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -1138,187 +1138,6 @@ int container_qt::findText( const QString& text ) return mFindMatches.size(); } -// // normalize Whitespace: multiple whitespaces to one -// std::string container_qt::normalizeWhitespace( const std::string& text ) -// { -// std::string normalized; -// bool lastWasSpace = false; - -// for ( char c : text ) -// { -// if ( std::isspace( static_cast( c ) ) ) -// { -// if ( !lastWasSpace && !normalized.empty() ) -// { -// normalized += ' '; -// lastWasSpace = true; -// } -// } -// else -// { -// normalized += c; -// lastWasSpace = false; -// } -// } - -// return normalized; -// } - -// void container_qt::collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText ) -// { -// if ( !el ) -// return; - -// std::string text; -// el->get_text( text ); -// if ( !text.empty() && el->is_text() ) -// { -// TextFragment fragment; -// fragment.text = text; -// fragment.element = el; -// fragment.pos = el->get_placement(); -// fragment.start_char_offset = static_cast( fullText.length() ); - -// std::string normalized = normalizeWhitespace( text ); -// fullText += normalized; - -// fragment.end_char_offset = static_cast( fullText.length() ); -// fragments.push_back( fragment ); - -// // Leerzeichen zwischen Elementen -// if ( !fullText.empty() && !std::isspace( fullText.back() ) ) -// { -// fullText += ' '; -// } -// } - -// for ( auto it = el->children().begin(); it != el->children().end(); ++it ) -// { -// collect_text_fragments( ( *it ), fragments, fullText ); -// } -// } - -// // calculate bounding box for all elements found in search -// litehtml::position container_qt::calculate_precise_bounding_box( const std::vector& allFragments, -// int searchStart, -// int searchEnd, -// std::vector& matchedFragments ) -// { -// litehtml::position boundingBox = { 0, 0, 0, 0 }; -// bool firstFragment = true; - -// for ( const auto& fragment : allFragments ) -// { -// // check if this fragment is part of the search -// int overlapStart = std::max( searchStart, fragment.start_char_offset ); -// int overlapEnd = std::min( searchEnd, fragment.end_char_offset ); - -// if ( overlapStart < overlapEnd ) -// { -// // yes this fragment is part of the search -// TextFragment matchedFragment = fragment; - -// // offsets relative to start -// int fragmentTextStart = overlapStart - fragment.start_char_offset; -// int fragmentTextEnd = overlapEnd - fragment.start_char_offset; - -// matchedFragment.start_char_offset = fragmentTextStart; -// matchedFragment.end_char_offset = fragmentTextEnd; - -// // calculate width of text -// // Importnat: use text_width from document_container -// std::string matchedText = fragment.text.substr( fragmentTextStart, fragmentTextEnd - fragmentTextStart ); - -// // estimated position (kann mit text_width verfeinert werden) -// litehtml::position fragmentPos = fragment.pos; - -// // for more precise positioning text_width would be helpfull -// // int textWidthBefore = container->text_width( -// // fragment.text.substr(0, fragmentTextStart).c_str(), -// // font -// // ); -// // fragmentPos.x += textWidthBefore; -// // fragmentPos.width = container->text_width(matchedText.c_str(), font); - -// // simplified approximation -// if ( fragment.text.length() > 0 ) -// { -// float charWidth = static_cast( fragment.pos.width ) / static_cast( fragment.text.length() ); -// fragmentPos.x += static_cast( charWidth * fragmentTextStart ); -// fragmentPos.width = static_cast( charWidth * matchedText.length() ); -// } - -// matchedFragments.push_back( matchedFragment ); - -// // extend Bounding Box -// if ( firstFragment ) -// { -// boundingBox = fragmentPos; -// firstFragment = false; -// } -// else -// { -// // Min/Max for containing Box -// int minX = std::min( boundingBox.x, fragmentPos.x ); -// int minY = std::min( boundingBox.y, fragmentPos.y ); -// int maxX = std::max( boundingBox.x + boundingBox.width, fragmentPos.x + fragmentPos.width ); -// int maxY = std::max( boundingBox.y + boundingBox.height, fragmentPos.y + fragmentPos.height ); - -// boundingBox.x = minX; -// boundingBox.y = minY; -// boundingBox.width = maxX - minX; -// boundingBox.height = maxY - minY; -// } -// } -// } - -// return boundingBox; -// } - -// // search document for Text including multiword phrases -// void container_qt::find_text_in_document( litehtml::document::ptr doc, -// const std::string& search_term, -// std::vector& matches, -// bool case_sensitive ) -// { -// if ( !doc || search_term.empty() ) -// return; - -// // Sammle alle Text-Fragmente mit Positionen -// std::vector fragments; -// std::string fullText; -// collect_text_fragments( doc->root(), fragments, fullText ); - -// // Normalisiere Text für Suche -// std::string normalizedFullText = normalizeWhitespace( fullText ); -// std::string normalizedSearchTerm = normalizeWhitespace( search_term ); - -// std::string findText = normalizedFullText; -// std::string searchFor = normalizedSearchTerm; - -// if ( !case_sensitive ) -// { -// std::transform( findText.begin(), findText.end(), findText.begin(), ::tolower ); -// std::transform( searchFor.begin(), searchFor.end(), searchFor.begin(), ::tolower ); -// } - -// // Alle Vorkommen finden -// size_t pos = 0; -// while ( ( pos = findText.find( searchFor, pos ) ) != std::string::npos ) -// { -// TextFindMatch match; -// match.matched_text = normalizedFullText.substr( pos, searchFor.length() ); - -// int searchStart = static_cast( pos ); -// int searchEnd = static_cast( pos + searchFor.length() ); - -// // Berechne präzise Bounding Box -// match.bounding_box = calculate_precise_bounding_box( fragments, searchStart, searchEnd, match.fragments ); - -// matches.push_back( match ); -// pos += searchFor.length(); -// } -// } // Textsuche durchführen int container_qt::find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive ) @@ -1384,11 +1203,13 @@ void container_qt::draw_highlights( litehtml::uint_ptr hdc ) for ( auto it = mFindMatches.begin(); it != mFindMatches.end(); ++it ) { const auto match = ( *it ); - for ( auto it = match.fragments.begin(); it != match.fragments.end(); ++it ) - { - litehtml::position pos = ( *it ).pos; - highlight_text_at_position( hdc, pos, match ); - } + // for ( auto it = match.fragments.begin(); it != match.fragments.end(); ++it ) + // { + // litehtml::position pos = ( *it ).pos; + // highlight_text_at_position( hdc, pos, match ); + // } + litehtml::position pos = ( *it ).bounding_box; + highlight_text_at_position( hdc, pos, match ); } } @@ -1471,11 +1292,6 @@ QString container_qt::selectedText() const return QString::fromStdString( m_textManager.selectedText( m_currentSelection ) ); } -// const DOMTextManager::SelectionRange& container_qt::getCurrentSelection() const -// { -// return m_currentSelection; -// } - void container_qt::clearSelection() { m_currentSelection.clear(); diff --git a/src/container_qt.h b/src/container_qt.h index 1f1fbff..2da617d 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -15,7 +15,14 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_container { + Q_OBJECT + +#ifdef UNIT_TEST + friend class FindTest; + friend class SelectionTest; +#endif + public: container_qt( QWidget* parent = nullptr ); @@ -93,7 +100,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void copySelectionToClipboard(); void clearSelection(); -private: +protected: // selection void drawSelection( QPainter& painter ); @@ -126,7 +133,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co QByteArray loadResource( Browser::ResourceType type, const QUrl& url ); MousePos convertMousePos( const QMouseEvent* event ); -private: +protected: int find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive = true ); bool find_next_match(); bool find_previous_match(); @@ -134,17 +141,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const TextManager::TextFindMatch& match ); void draw_highlights( litehtml::uint_ptr hdc ); void clear_highlights() { mFindMatches.clear(); } - // std::string normalizeWhitespace( const std::string& text ); - // void find_text_in_document( litehtml::document::ptr doc, - // const std::string& search_term, - // std::vector& matches, - // bool case_sensitive = true ); - // void collect_text_fragments( litehtml::element::ptr el, std::vector& fragments, std::string& fullText - // ); litehtml::position calculate_precise_bounding_box( const std::vector& allFragments, - // int searchStart, - // int searchEnd, - // std::vector& matchedFragments ); - void scroll_to_find_match( const TextManager::TextFindMatch* ); + void scroll_to_find_match( const TextManager::TextFindMatch* ); std::shared_ptr mDocument; QByteArray mDocumentSource; diff --git a/test/library/CMakeLists.txt b/test/library/CMakeLists.txt index 2ca2bfe..4e268a3 100644 --- a/test/library/CMakeLists.txt +++ b/test/library/CMakeLists.txt @@ -8,6 +8,8 @@ else() set(QT_VERSION_MAJOR 5) endif() +add_compile_definitions(-DUNIT_TEST) + set( test_names test_html_content test_css_content @@ -39,9 +41,9 @@ foreach( name ${test_names}) ${${name}_mocs} ) - target_include_directories( ${name} PRIVATE ${QLiteHtmlBrowser_SOURCE_DIR}/include ) + target_include_directories( ${name} PRIVATE ${QLiteHtmlBrowser_SOURCE_DIR}/include ${QLiteHtmlBrowser_SOURCE_DIR}/src) - target_link_libraries(${name} PRIVATE QLiteHtmlBrowser Qt::Widgets Qt::Gui Qt::Test ) + target_link_libraries(${name} PRIVATE QLiteHtmlBrowser litehtml Qt::Widgets Qt::Gui Qt::Test ) target_compile_definitions(${name} PRIVATE TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/test/library/test_find.cpp b/test/library/test_find.cpp index d571e37..1abc199 100644 --- a/test/library/test_find.cpp +++ b/test/library/test_find.cpp @@ -1,6 +1,7 @@ #include "test_find.h" #include - +#include "QLiteHtmlBrowserImpl.h" +#include "container_qt.h" #include #include @@ -24,8 +25,20 @@ QLiteHtmlBrowser* FindTest::createMainWindow( const QSize& size ) return browser; } +void FindTest::test_find_single_word_data() +{ + QTest::addColumn( "find_text" ); + QTest::addColumn( "matches" ); + QTest::addRow( "%d", 0 ) << QString( "Sätze" ) << 40; + QTest::addRow( "%d", 1 ) << QString( "Er" ) << 46; + QTest::addRow( "%d", 2 ) << QString( "" ) << 0; +} + void FindTest::test_find_single_word() { + QFETCH( QString, find_text ); + QFETCH( int, matches ); + auto browser = createMainWindow( mBrowserSize ); // QVERIFY( mWnd->centralWidget() ); @@ -34,21 +47,29 @@ void FindTest::test_find_single_word() auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->findText( ( "Sätze" ) ); - qApp->processEvents(); - QCOMPARE( found, 40 ); - found = browser->findText( ( " Er" ) ); + auto found = browser->findText( ( find_text ) ); qApp->processEvents(); - QCOMPARE( found, 46 ); + QCOMPARE( found, matches ); +} - found = browser->findText( "" ); - qApp->processEvents(); - QCOMPARE( found, 0 ); +void FindTest::test_find_phrase_data() +{ + QTest::addColumn( "find_text" ); + QTest::addColumn( "matches" ); + QTest::addColumn>( "bounding_boxes" ); + QList boxes = { { 252, 995, 151, 17 } }; + QTest::addRow( "%d", 0 ) << QString( "allgemeine Beobachtungen" ) << 1 << boxes; + boxes = { { 0, 0, 0, 0 } }; + QTest::addRow( "%d", 0 ) << QString( "schliesst ab" ) << 0 << boxes; } void FindTest::test_find_phrase() { + QFETCH( QString, find_text ); + QFETCH( int, matches ); + QFETCH( QList, bounding_boxes ); + auto browser = createMainWindow( mBrowserSize ); // QVERIFY( mWnd->centralWidget() ); @@ -57,24 +78,57 @@ void FindTest::test_find_phrase() auto base = QString{ TEST_SOURCE_DIR } + "/search/"; browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->findText( "allgemeine Beobachtungen" ); + auto found = browser->findText( find_text ); qApp->processEvents(); - QCOMPARE( found, 1 ); + QCOMPARE( found, matches ); + + if ( 0 < found ) + { + auto container = browser->mImpl->container(); + auto find_machtes = container->mFindMatches; + for ( size_t i = 0; i < find_machtes.size(); ++i ) + { + const auto& match = find_machtes[i]; + QCOMPARE( QRect( match.bounding_box.x, match.bounding_box.y, match.bounding_box.width, match.bounding_box.height ), bounding_boxes[i] ); + // qDebug() << "Find match" << i << ":" + // << "Text:" << QString::fromStdString( match.matched_text ) << "Pos X" << match.bounding_box.x; + } + } +} - found = browser->findText( "schliesst ab" ); - qApp->processEvents(); - QCOMPARE( found, 0 ); +void FindTest::test_find_phrase_multi_element_data() +{ + QTest::addColumn( "find_text" ); + QTest::addColumn( "matches" ); + QTest::addColumn>( "bounding_boxes" ); + QList boxes = { { 8, 2145, 64, 46 } }; + QTest::addRow( "%d", 0 ) << QString( "schließt ab.Absatz 48" ) << 1 << boxes; } void FindTest::test_find_phrase_multi_element() { + QFETCH( QString, find_text ); + QFETCH( int, matches ); + QFETCH( QList, bounding_boxes ); + auto browser = createMainWindow( mBrowserSize ); // QVERIFY( mWnd->centralWidget() ); auto base = QString{ TEST_SOURCE_DIR } + "/search/"; - browser->setSource( QUrl::fromLocalFile( base + "/LongGermanText.html" ) ); - auto found = browser->findText( QStringLiteral( "schließt ab.Absatz 48" ) ); + + auto found = browser->findText( find_text ); qApp->processEvents(); - QCOMPARE( found, 1 ); + QCOMPARE( found, matches ); + if ( 0 < found ) + { + + auto container = browser->mImpl->container(); + auto find_machtes = container->mFindMatches; + for ( size_t i = 0; i < find_machtes.size(); ++i ) + { + const auto& match = find_machtes[i]; + QCOMPARE( QRect( match.bounding_box.x, match.bounding_box.y, match.bounding_box.width, match.bounding_box.height ), bounding_boxes[i] ); + } + } } diff --git a/test/library/test_find.h b/test/library/test_find.h index c88c610..9a15d7e 100644 --- a/test/library/test_find.h +++ b/test/library/test_find.h @@ -21,8 +21,11 @@ private Q_SLOTS: mWnd.reset(); mWnd = nullptr; } + void test_find_single_word_data(); void test_find_single_word(); + void test_find_phrase_data(); void test_find_phrase(); + void test_find_phrase_multi_element_data(); void test_find_phrase_multi_element(); private: From d58a5aaee282edbeb60e9f98c105e26aac7981df Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Fri, 31 Oct 2025 11:17:41 +0100 Subject: [PATCH 09/15] table output from selection formatted --- src/QLiteHtmlBrowserImpl.h | 2 +- src/TextManager.cpp | 115 +++++++++++++++++-- src/TextManager.h | 18 +-- src/container_qt.cpp | 14 ++- src/container_qt.h | 13 ++- test/library/CMakeLists.txt | 15 ++- test/library/selection/simple_inline.html | 5 + test/library/selection/simple_paragraph.html | 7 ++ test/library/selection/simple_table.html | 21 ++++ test/library/selection/simple_text.html | 5 + test/library/test_selection.cpp | 58 ++++++++++ test/library/test_selection.h | 32 ++++++ 12 files changed, 278 insertions(+), 27 deletions(-) create mode 100644 test/library/selection/simple_inline.html create mode 100644 test/library/selection/simple_paragraph.html create mode 100644 test/library/selection/simple_table.html create mode 100644 test/library/selection/simple_text.html create mode 100644 test/library/test_selection.cpp create mode 100644 test/library/test_selection.h diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 3bb6479..95b6253 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -84,7 +84,7 @@ class QLiteHtmlBrowserImpl : public QWidget void selectionChanged(); #ifdef UNIT_TEST - container_qt* const container() const { return mContainer; } + container_qt* container() const { return mContainer; } #endif private: diff --git a/src/TextManager.cpp b/src/TextManager.cpp index 3274dd9..ea7080c 100644 --- a/src/TextManager.cpp +++ b/src/TextManager.cpp @@ -238,6 +238,12 @@ void TextManager::collectTextFragments( litehtml::element::ptr el ) fragment.global_end_offset = m_fullText.length(); // Ende NACH dem Text fragment.element_tag = getElementTag( el ); fragment.is_block_element = isBlockLevelElement( fragment.element_tag ); + fragment.table_cell_parent = findParentWithTag( el, "td" ); + if ( !fragment.table_cell_parent ) + { + fragment.table_cell_parent = findParentWithTag( el, "th" ); + } + fragment.table_row_parent = findParentWithTag( el, "tr" ); m_fragments.push_back( fragment ); @@ -257,6 +263,31 @@ void TextManager::collectTextFragments( litehtml::element::ptr el ) } } +litehtml::element::ptr TextManager::findParentWithTag( litehtml::element::ptr element, const std::string& tag ) const +{ + if ( !element ) + return nullptr; + + auto current = element->parent(); + while ( current ) + { + const char* tagName = current->get_tagName(); + if ( tagName ) + { + std::string currentTag( tagName ); + std::transform( currentTag.begin(), currentTag.end(), currentTag.begin(), ::tolower ); + + if ( currentTag == tag ) + { + return current; + } + } + current = current->parent(); + } + + return nullptr; +} + litehtml::position TextManager::calculateBoundingBox( int searchStart, int searchEnd, std::vector& matchedFragments ) { litehtml::position boundingBox = { 0, 0, 0, 0 }; @@ -389,12 +420,12 @@ std::string TextManager::getElementTag( litehtml::element::ptr element ) const bool TextManager::isBlockLevelElement( const std::string& tag ) const { // Liste der gängigen Block-Level-Elemente - static const std::vector blockElements = { - "address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", - "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main", - "nav", "noscript", "ol", "p", "pre", "section", "table", "tfoot", "ul", "video", - "br" // BR ist speziell: inline aber erzeugt Zeilenumbruch - }; + static const std::vector blockElements = { "address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl", + "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", + "h3", "h4", "h5", "h6", "header", "hr", "li", "main", + "nav", "noscript", "ol", "p", "pre", "section", "ul", "video", + "br", // BR ist speziell: inline aber erzeugt Zeilenumbruch + "table", "thead", "tbody", "tfoot" }; return std::find( blockElements.begin(), blockElements.end(), tag ) != blockElements.end(); } @@ -440,10 +471,14 @@ std::string TextManager::selectedText( const SelectionRange& selection, TextForm { const auto& prevFragment = selection.fragments[i - 1]; - // WICHTIG: Prüfe ob gleicher Parent + if ( prevFragment.table_row_parent != fragment.table_row_parent ) + { + // part = "\n" + part; + result += "\n"; + } + if ( prevFragment.parent_element != fragment.parent_element ) { - // Unterschiedliches Parent-Element if ( prevFragment.element_tag == "p" && fragment.element_tag == "p" ) { // Zwischen zwei

-Elementen: Doppelter Newline @@ -458,14 +493,27 @@ std::string TextManager::selectedText( const SelectionRange& selection, TextForm { if ( prevFragment.element_tag != fragment.element_tag ) { - // Inline-Elemente mit unterschiedlichem Parent: Space - result += ' '; + if ( !result.empty() && !std::isspace( result.back() ) && !std::isspace( part.front() ) ) + { + result += ' '; + } + } + else + { + if ( !result.empty() && !std::isspace( result.back() ) && !std::isspace( part.front() ) ) + { + result += ' '; + } } } } else { - // Gleiches Parent-Element: Nur Space result += ' '; + + // if ( !result.empty() && !std::isspace( result.back() ) ) + // { + // result += ' '; + // } } } @@ -503,3 +551,48 @@ std::string TextManager::selectedText( const SelectionRange& selection, TextForm return result.substr( start, end - start + 1 ); } + +TextManager::SelectionRange TextManager::selectAll() +{ + SelectionRange selection; + + // Wenn keine Fragmente vorhanden, leere Selektion zurückgeben + if ( m_fragments.empty() ) + { + return selection; + } + + // Start-Position: Erstes Fragment, erstes Zeichen + const auto& firstFragment = m_fragments.front(); + selection.start.element = firstFragment.element; + selection.start.char_offset = 0; + selection.start.element_pos = firstFragment.pos; + + // End-Position: Letztes Fragment, letztes Zeichen + const auto& lastFragment = m_fragments.back(); + selection.end.element = lastFragment.element; + selection.end.char_offset = lastFragment.text.length(); + selection.end.element_pos = lastFragment.pos; + + // Alle Fragmente zur Selektion hinzufügen + for ( const auto& fragment : m_fragments ) + { + if ( !fragment.hasValidPosition() ) + { + continue; + } + + TextFragment selectedFragment = fragment; + + // Vollständiges Fragment auswählen + selectedFragment.start_char_offset = 0; + selectedFragment.end_char_offset = fragment.text.length(); + + // Position bleibt unverändert (gesamtes Fragment) + selectedFragment.pos = fragment.pos; + + selection.fragments.push_back( selectedFragment ); + } + + return selection; +} diff --git a/src/TextManager.h b/src/TextManager.h index 2db1764..feaccc0 100644 --- a/src/TextManager.h +++ b/src/TextManager.h @@ -18,8 +18,10 @@ class TextManager bool is_leaf; litehtml::element::ptr parent_element; - std::string element_tag; // z.B. "p", "div", "span", "br" - bool is_block_element; // true für Block-Level-Elemente + std::string element_tag; // z.B. "p", "div", "span", "br" + bool is_block_element; // true für Block-Level-Elemente + litehtml::element::ptr table_cell_parent; // oder + litehtml::element::ptr table_row_parent; // bool hasValidPosition() const { return pos.width > 0 && pos.height > 0; } }; @@ -84,11 +86,13 @@ class TextManager SelectionRange getSelectionBetween( const TextPosition& start, const TextPosition& end ); std::vector findText( const std::string& search_term, bool case_sensitive = true ); std::string selectedText( const SelectionRange& selection, TextFormatMode mode = TextFormatMode::Structured ) const; + SelectionRange selectAll(); private: - void collectTextFragments( litehtml::element::ptr el ); - litehtml::position calculateBoundingBox( int searchStart, int searchEnd, std::vector& matchedFragments ); - bool isBlockLevelElement( const std::string& tag ) const; - std::string getElementTag( litehtml::element::ptr element ) const; - bool shouldInsertNewline( const TextFragment& prev, const TextFragment& current ) const; + void collectTextFragments( litehtml::element::ptr el ); + litehtml::position calculateBoundingBox( int searchStart, int searchEnd, std::vector& matchedFragments ); + bool isBlockLevelElement( const std::string& tag ) const; + std::string getElementTag( litehtml::element::ptr element ) const; + bool shouldInsertNewline( const TextFragment& prev, const TextFragment& current ) const; + litehtml::element::ptr findParentWithTag( litehtml::element::ptr element, const std::string& tag ) const; }; diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 91e7d4a..0ef877b 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -1138,7 +1138,6 @@ int container_qt::findText( const QString& text ) return mFindMatches.size(); } - // Textsuche durchführen int container_qt::find_text( litehtml::document::ptr doc, const std::string& search_term, bool case_sensitive ) { @@ -1296,6 +1295,7 @@ void container_qt::clearSelection() { m_currentSelection.clear(); viewport()->update(); + emit selectionChanged(); } void container_qt::keyPressEvent( QKeyEvent* event ) @@ -1310,6 +1310,11 @@ void container_qt::keyPressEvent( QKeyEvent* event ) { clearSelection(); } + // Strg+A alles selektieren + else if ( event->matches( QKeySequence::SelectAll ) ) + { + selectAll(); + } QWidget::keyPressEvent( event ); } @@ -1348,3 +1353,10 @@ void container_qt::drawSelection( QPainter& painter ) painter.restore(); } + +void container_qt::selectAll() +{ + m_currentSelection = m_textManager.selectAll(); + viewport()->update(); + emit selectionChanged(); +} diff --git a/src/container_qt.h b/src/container_qt.h index 2da617d..b2b5fe3 100644 --- a/src/container_qt.h +++ b/src/container_qt.h @@ -99,6 +99,7 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co // const DOMTextManager::SelectionRange& getCurrentSelection() const; void copySelectionToClipboard(); void clearSelection(); + void selectAll(); protected: // selection @@ -139,9 +140,9 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co bool find_previous_match(); const TextManager::TextFindMatch* find_current_match() const; void highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const TextManager::TextFindMatch& match ); - void draw_highlights( litehtml::uint_ptr hdc ); - void clear_highlights() { mFindMatches.clear(); } - void scroll_to_find_match( const TextManager::TextFindMatch* ); + void draw_highlights( litehtml::uint_ptr hdc ); + void clear_highlights() { mFindMatches.clear(); } + void scroll_to_find_match( const TextManager::TextFindMatch* ); std::shared_ptr mDocument; QByteArray mDocumentSource; @@ -164,9 +165,9 @@ class container_qt : public QAbstractScrollArea, protected litehtml::document_co litehtml::position mClip = {}; std::vector mFindMatches = {}; int mFindCurrentMatchIndex = -1; - QColor mHighlightColor = QColor( 255, 255, 0, 30 ); - QColor mSelectionColor = QColor( 0, 120, 215, 100 ); - QString mFindText; + QColor mHighlightColor = QColor( 255, 255, 0, 30 ); + QColor mSelectionColor = QColor( 0, 120, 215, 100 ); + QString mFindText; // for selection diff --git a/test/library/CMakeLists.txt b/test/library/CMakeLists.txt index 4e268a3..df1aabd 100644 --- a/test/library/CMakeLists.txt +++ b/test/library/CMakeLists.txt @@ -15,6 +15,7 @@ set( test_names test_css_content test_history test_find + test_selection ) set( runpath "$;$") @@ -31,6 +32,10 @@ foreach( name ${test_names}) qt_wrap_cpp ( ${name}_mocs ${name}.h test_base.h + ${QLiteHtmlBrowser_SOURCE_DIR}/src/container_qt.h + ${QLiteHtmlBrowser_SOURCE_DIR}/src/QLiteHtmlBrowserImpl.h + ${QLiteHtmlBrowser_SOURCE_DIR}/src/TextManager.h + ${QLiteHtmlBrowser_SOURCE_DIR}/include/qlitehtmlbrowser/QLiteHtmlBrowser.h ) target_sources(${name} @@ -39,11 +44,19 @@ foreach( name ${test_names}) ${name}.h test_base.h ${${name}_mocs} + ${QLiteHtmlBrowser_SOURCE_DIR}/include/qlitehtmlbrowser/QLiteHtmlBrowser.h + ${QLiteHtmlBrowser_SOURCE_DIR}/src/QLiteHtmlBrowser.cpp + ${QLiteHtmlBrowser_SOURCE_DIR}/src/QLiteHtmlBrowserImpl.cpp + ${QLiteHtmlBrowser_SOURCE_DIR}/src/QLiteHtmlBrowserImpl.h + ${QLiteHtmlBrowser_SOURCE_DIR}/src/container_qt.h + ${QLiteHtmlBrowser_SOURCE_DIR}/src/container_qt.cpp + ${QLiteHtmlBrowser_SOURCE_DIR}/src/TextManager.cpp + ${QLiteHtmlBrowser_SOURCE_DIR}/src/TextManager.h ) target_include_directories( ${name} PRIVATE ${QLiteHtmlBrowser_SOURCE_DIR}/include ${QLiteHtmlBrowser_SOURCE_DIR}/src) - target_link_libraries(${name} PRIVATE QLiteHtmlBrowser litehtml Qt::Widgets Qt::Gui Qt::Test ) + target_link_libraries(${name} PUBLIC litehtml Qt::Widgets Qt::Gui Qt::Test ) target_compile_definitions(${name} PRIVATE TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/test/library/selection/simple_inline.html b/test/library/selection/simple_inline.html new file mode 100644 index 0000000..d7e1611 --- /dev/null +++ b/test/library/selection/simple_inline.html @@ -0,0 +1,5 @@ + + +This is bold text with one strong word + + diff --git a/test/library/selection/simple_paragraph.html b/test/library/selection/simple_paragraph.html new file mode 100644 index 0000000..5afa1e1 --- /dev/null +++ b/test/library/selection/simple_paragraph.html @@ -0,0 +1,7 @@ + + +

Abschnitt 1: Erste Zeile

+

Abschnitt 2: Zweite Zeile

+

Abschnitt 3: Dritte Zeile

+ + diff --git a/test/library/selection/simple_table.html b/test/library/selection/simple_table.html new file mode 100644 index 0000000..545d2c1 --- /dev/null +++ b/test/library/selection/simple_table.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + +
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
+ + diff --git a/test/library/selection/simple_text.html b/test/library/selection/simple_text.html new file mode 100644 index 0000000..7df49c6 --- /dev/null +++ b/test/library/selection/simple_text.html @@ -0,0 +1,5 @@ + + +This is pure text only in one sentence + + diff --git a/test/library/test_selection.cpp b/test/library/test_selection.cpp new file mode 100644 index 0000000..d1193dc --- /dev/null +++ b/test/library/test_selection.cpp @@ -0,0 +1,58 @@ +#include "test_selection.h" +#include +#include "QLiteHtmlBrowserImpl.h" +#include "container_qt.h" +#include +#include + +int main( int argc, char** argv ) +{ + QApplication app( argc, argv ); + + SelectionTest mContentTest; + return QTest::qExec( &mContentTest, mContentTest.args() ); +} + +QLiteHtmlBrowser* SelectionTest::createMainWindow( const QSize& size ) +{ + mWnd = std::make_unique(); + auto browser = new QLiteHtmlBrowser( nullptr ); + mWnd->setCentralWidget( browser ); + browser->setMinimumSize( size ); + browser->update(); + browser->show(); + mWnd->show(); + return browser; +} + +void SelectionTest::testSelectionText_data() +{ + QTest::addColumn( "html" ); + QTest::addColumn( "selection" ); + + QTest::addRow( "%d", 0 ) << QString( "simple_text.html" ) << QString( "This is pure text only in one sentence" ); + QTest::addRow( "%d", 1 ) << QString( "simple_inline.html" ) << QString( "This is bold text with one strong word" ); + QTest::addRow( "%d", 2 ) << QString( "simple_paragraph.html" ) + << QString( "Abschnitt 1: Erste Zeile\n\nAbschnitt 2: Zweite Zeile\n\nAbschnitt 3: Dritte Zeile" ); + QTest::addRow( "%d", 3 ) + << QString( "simple_table.html" ) + << QString( "Company Contact Country\nAlfreds Futterkiste Maria Anders Germany\nCentro comercial Moctezuma Francisco Chang Mexico" ); +} + +void SelectionTest::testSelectionText() +{ + QFETCH( QString, html ); + QFETCH( QString, selection ); + + auto browser = createMainWindow( mBrowserSize ); + auto base = QString{ TEST_SOURCE_DIR } + "/selection/"; + + browser->setSource( QUrl::fromLocalFile( base + "/" + html ) ); + + auto container = browser->mImpl->container(); + + container->selectAll(); + + auto text = container->selectedText(); + QCOMPARE( text, selection ); +} diff --git a/test/library/test_selection.h b/test/library/test_selection.h new file mode 100644 index 0000000..fd5b393 --- /dev/null +++ b/test/library/test_selection.h @@ -0,0 +1,32 @@ + +#include +#include +#include "test_base.h" + +class QLiteHtmlBrowser; + +class SelectionTest : public TestBase +{ + Q_OBJECT +public: + SelectionTest() + : TestBase( qApp->arguments() ){}; + virtual ~SelectionTest() = default; + +private Q_SLOTS: + void init() { TestBase::init(); } + void cleanup() + { + TestBase::cleanup(); + mWnd.reset(); + mWnd = nullptr; + } + + void testSelectionText_data(); + void testSelectionText(); + +private: + QLiteHtmlBrowser* createMainWindow( const QSize& ); + std::unique_ptr mWnd = nullptr; + QSize mBrowserSize = { 800, 600 }; +}; From 4038da57d5ce5f84c2b1af2a8411e5ac341a66ee Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Fri, 31 Oct 2025 12:39:40 +0100 Subject: [PATCH 10/15] format fixes --- test/library/test_selection.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/library/test_selection.h b/test/library/test_selection.h index fd5b393..ed2ef7d 100644 --- a/test/library/test_selection.h +++ b/test/library/test_selection.h @@ -10,7 +10,7 @@ class SelectionTest : public TestBase Q_OBJECT public: SelectionTest() - : TestBase( qApp->arguments() ){}; + : TestBase( qApp->arguments() ) {}; virtual ~SelectionTest() = default; private Q_SLOTS: From 1e33523a36e98206a24b7558a6e05c6ef2b08681 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Fri, 31 Oct 2025 13:24:01 +0100 Subject: [PATCH 11/15] fix format with newer clang-format --- test/library/test_css_content.h | 2 +- test/library/test_find.h | 2 +- test/library/test_history.h | 2 +- test/library/test_html_content.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/library/test_css_content.h b/test/library/test_css_content.h index 6b050e3..dc82531 100644 --- a/test/library/test_css_content.h +++ b/test/library/test_css_content.h @@ -10,7 +10,7 @@ class HTMLCssTest : public TestBase Q_OBJECT public: HTMLCssTest() - : TestBase( qApp->arguments() ){}; + : TestBase( qApp->arguments() ) {}; virtual ~HTMLCssTest() = default; private Q_SLOTS: diff --git a/test/library/test_find.h b/test/library/test_find.h index 9a15d7e..dab4f41 100644 --- a/test/library/test_find.h +++ b/test/library/test_find.h @@ -10,7 +10,7 @@ class FindTest : public TestBase Q_OBJECT public: FindTest() - : TestBase( qApp->arguments() ){}; + : TestBase( qApp->arguments() ) {}; virtual ~FindTest() = default; private Q_SLOTS: diff --git a/test/library/test_history.h b/test/library/test_history.h index bc46c0d..e62b700 100644 --- a/test/library/test_history.h +++ b/test/library/test_history.h @@ -10,7 +10,7 @@ class HistoryTest : public TestBase Q_OBJECT public: HistoryTest() - : TestBase( qApp->arguments() ){}; + : TestBase( qApp->arguments() ) {}; virtual ~HistoryTest() = default; private Q_SLOTS: diff --git a/test/library/test_html_content.h b/test/library/test_html_content.h index 7a688e3..e61250a 100644 --- a/test/library/test_html_content.h +++ b/test/library/test_html_content.h @@ -10,7 +10,7 @@ class HTMLContentTest : public TestBase Q_OBJECT public: HTMLContentTest() - : TestBase( qApp->arguments() ){}; + : TestBase( qApp->arguments() ) {}; virtual ~HTMLContentTest() = default; private Q_SLOTS: From bd68c21d143ba8c83f78e8b7f4976b9e62925a13 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Fri, 31 Oct 2025 13:25:26 +0100 Subject: [PATCH 12/15] bump to version 2.3.0 --- CHANGELOG.md | 5 +++++ CMakeLists.txt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba227a..ab06029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v2.3.0 + +- [#5](https://github.com/procitec/qlitehtmlbrowser/issues/5): Implement selection + + ## v2.2.1 - [#17](https://github.com/procitec/qlitehtmlbrowser/issues/17): Scale changed signal diff --git a/CMakeLists.txt b/CMakeLists.txt index 56df0b7..2c710b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.28) -project( QLiteHtmlBrowser VERSION 2.2.1 ) +project( QLiteHtmlBrowser VERSION 2.3.0 ) include(GNUInstallDirs) From 15228ceb50b68241dbc25e90bc80cf653a68765f Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 3 Nov 2025 10:49:21 +0100 Subject: [PATCH 13/15] skip tests on unsupported QT_QPA_PLUGIN --- test/library/test_find.cpp | 43 ++++++++++++++++++++++++++++++++++---- test/library/test_find.h | 1 + 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/test/library/test_find.cpp b/test/library/test_find.cpp index 1abc199..0355e6a 100644 --- a/test/library/test_find.cpp +++ b/test/library/test_find.cpp @@ -13,6 +13,18 @@ int main( int argc, char** argv ) return QTest::qExec( &mContentTest, mContentTest.args() ); } +bool FindTest::skipUITest() const +{ + auto skip = false; + auto qpa = qgetenv( "QT_QPA_PLATFORM" ); + if ( !qpa.isEmpty() ) + { + auto value = QString::fromLocal8Bit( qpa ); + skip = value != "xcb"; + } + return skip; +} + QLiteHtmlBrowser* FindTest::createMainWindow( const QSize& size ) { mWnd = std::make_unique(); @@ -84,14 +96,24 @@ void FindTest::test_find_phrase() if ( 0 < found ) { + if ( skipUITest() ) + { + QSKIP( "skip tests due QT_QPA_PLATFORM_PLUGIN" ); + } + auto container = browser->mImpl->container(); auto find_machtes = container->mFindMatches; for ( size_t i = 0; i < find_machtes.size(); ++i ) { const auto& match = find_machtes[i]; - QCOMPARE( QRect( match.bounding_box.x, match.bounding_box.y, match.bounding_box.width, match.bounding_box.height ), bounding_boxes[i] ); - // qDebug() << "Find match" << i << ":" - // << "Text:" << QString::fromStdString( match.matched_text ) << "Pos X" << match.bounding_box.x; + auto bb = QRect( match.bounding_box.x, match.bounding_box.y, match.bounding_box.width, match.bounding_box.height ); + if ( bb != bounding_boxes[i] ) + { + auto img = container->grab(); // QMainWindow, QWidget usw. + img.save( QString( "screenshot_ui_find_phrase_%1.png" ).arg( i ) ); + } + + QCOMPARE( bb, bounding_boxes[i] ); } } } @@ -119,16 +141,29 @@ void FindTest::test_find_phrase_multi_element() auto found = browser->findText( find_text ); qApp->processEvents(); + // QTest::qWaitForWindowExposed( browser->mImpl->container() ); QCOMPARE( found, matches ); if ( 0 < found ) { + if ( skipUITest() ) + { + QSKIP( "skip tests due QT_QPA_PLATFORM_PLUGIN" ); + } auto container = browser->mImpl->container(); + qApp->processEvents(); auto find_machtes = container->mFindMatches; for ( size_t i = 0; i < find_machtes.size(); ++i ) { const auto& match = find_machtes[i]; - QCOMPARE( QRect( match.bounding_box.x, match.bounding_box.y, match.bounding_box.width, match.bounding_box.height ), bounding_boxes[i] ); + auto bb = QRect( match.bounding_box.x, match.bounding_box.y, match.bounding_box.width, match.bounding_box.height ); + if ( bb != bounding_boxes[i] ) + { + auto img = container->grab(); // QMainWindow, QWidget usw. + img.save( QDir::currentPath() + QString( "/screenshot_ui_multi_element_%1.png" ).arg( i ) ); + } + + QCOMPARE( bb, bounding_boxes[i] ); } } } diff --git a/test/library/test_find.h b/test/library/test_find.h index dab4f41..ba5a679 100644 --- a/test/library/test_find.h +++ b/test/library/test_find.h @@ -30,6 +30,7 @@ private Q_SLOTS: private: QLiteHtmlBrowser* createMainWindow( const QSize& ); + bool skipUITest() const; std::unique_ptr mWnd = nullptr; QSize mBrowserSize = { 800, 600 }; }; From 785c2558ecc34f590b11ec25f4e5f61297165abb Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 3 Nov 2025 10:51:52 +0100 Subject: [PATCH 14/15] skip tests on unsupported QT_QPA_PLUGIN --- test/library/test_find.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/library/test_find.cpp b/test/library/test_find.cpp index 0355e6a..f2c44a5 100644 --- a/test/library/test_find.cpp +++ b/test/library/test_find.cpp @@ -150,7 +150,7 @@ void FindTest::test_find_phrase_multi_element() QSKIP( "skip tests due QT_QPA_PLATFORM_PLUGIN" ); } - auto container = browser->mImpl->container(); + auto container = browser->mImpl->container(); qApp->processEvents(); auto find_machtes = container->mFindMatches; for ( size_t i = 0; i < find_machtes.size(); ++i ) From 933bdda9299fe8777a0d13f9dfd4731949e005c1 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 3 Nov 2025 13:25:54 +0100 Subject: [PATCH 15/15] improve individuall character selection --- src/container_qt.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/container_qt.cpp b/src/container_qt.cpp index 0ef877b..0857156 100644 --- a/src/container_qt.cpp +++ b/src/container_qt.cpp @@ -878,10 +878,12 @@ void container_qt::setScale( double scale ) ( 0 < horizontalScrollBar()->maximum() ) ? static_cast( horizontalScrollBar()->value() ) / horizontalScrollBar()->maximum() : 0.0; auto relV = ( 0 < verticalScrollBar()->maximum() ) ? static_cast( verticalScrollBar()->value() ) / verticalScrollBar()->maximum() : 0.0; mScale = std::clamp( scale, mMinScale, mMaxScale ); + m_currentSelection.clear(); render(); - findText( mFindText ); horizontalScrollBar()->setValue( std::floor( relH * horizontalScrollBar()->maximum() ) ); verticalScrollBar()->setValue( std::floor( relV * verticalScrollBar()->maximum() ) ); + m_textManager.buildFromDocument( mDocument ); + findText( mFindText ); emit scaleChanged(); } @@ -915,9 +917,13 @@ void container_qt::resizeEvent( QResizeEvent* event ) ( 0 < horizontalScrollBar()->maximum() ) ? static_cast( horizontalScrollBar()->value() ) / horizontalScrollBar()->maximum() : 0.0; auto relV = ( 0 < verticalScrollBar()->maximum() ) ? static_cast( verticalScrollBar()->value() ) / verticalScrollBar()->maximum() : 0.0; QAbstractScrollArea::resizeEvent( event ); + m_currentSelection.clear(); render(); horizontalScrollBar()->setValue( std::floor( relH * horizontalScrollBar()->maximum() ) ); verticalScrollBar()->setValue( std::floor( relV * verticalScrollBar()->maximum() ) ); + + m_textManager.buildFromDocument( mDocument ); + findText( mFindText ); } void container_qt::wheelEvent( QWheelEvent* e ) @@ -1216,8 +1222,9 @@ void container_qt::draw_highlights( litehtml::uint_ptr hdc ) void container_qt::highlight_text_at_position( litehtml::uint_ptr hdc, const litehtml::position& pos, const TextManager::TextFindMatch& match ) { - qDebug() << "Highlighting text '" << QString::fromStdString( match.matched_text ) << "' at position (" << pos.x << ", " << pos.y << ") with size " - << pos.width << "x" << pos.height; + // qDebug() << "Highlighting text '" << QString::fromStdString( match.matched_text ) << "' at position (" << pos.x << ", " << pos.y << ") with size + // " + // << pos.width << "x" << pos.height; QPainter* p( reinterpret_cast( hdc ) ); p->save(); @@ -1338,7 +1345,8 @@ void container_qt::drawSelection( QPainter& painter ) } int startX = pos.x + static_cast( charWidth * fragment.start_char_offset ); - int width = static_cast( charWidth * ( fragment.end_char_offset - fragment.start_char_offset ) ); + // int width = static_cast( charWidth * ( fragment.end_char_offset - fragment.start_char_offset ) ); + int width = pos.width; auto scroll_pos = -scrollBarPos();