diff --git a/autotests/searchtest.cpp b/autotests/searchtest.cpp --- a/autotests/searchtest.cpp +++ b/autotests/searchtest.cpp @@ -46,6 +46,8 @@ void testHyphenAtEndOfLineWithoutYOverlap(); void testHyphenWithYOverlap(); void testHyphenAtEndOfPage(); + void testWholeWords_data(); + void testWholeWords(); void testOneColumn(); void testTwoColumns(); }; @@ -78,9 +80,9 @@ Okular::TextPage* tp; \ createTextPage(text, rect, tp, page); -#define TEST_NEXT_PREV(searchType, expectedStatus) \ +#define TEST_NEXT_PREV(searchType, expectedStatus, wholeWords) \ { \ - Okular::RegularAreaRect* result = tp->findText(0, searchString, searchType, Qt::CaseSensitive, NULL); \ + Okular::RegularAreaRect* result = tp->findText(0, searchString, searchType, Qt::CaseSensitive, NULL, wholeWords); \ QCOMPARE(!!result, expectedStatus); \ delete result; \ } @@ -148,17 +150,17 @@ //Test 3 of the 8 cases listed above: //FromTop, Next-Next (match) and Next-Next (no match) - TEST_NEXT_PREV(Okular::FromTop, true); - TEST_NEXT_PREV(Okular::NextResult, true); - TEST_NEXT_PREV(Okular::NextResult, false); + TEST_NEXT_PREV(Okular::FromTop, true, false); + TEST_NEXT_PREV(Okular::NextResult, true, false); + TEST_NEXT_PREV(Okular::NextResult, false, false); //Test 5 cases: FromBottom, Previous-Previous (match), Previous-Next, //Next-Previous, Previous-Previous (no match) - TEST_NEXT_PREV(Okular::FromBottom, true); - TEST_NEXT_PREV(Okular::PreviousResult, true); - TEST_NEXT_PREV(Okular::NextResult, true); - TEST_NEXT_PREV(Okular::PreviousResult, true); - TEST_NEXT_PREV(Okular::PreviousResult, false); + TEST_NEXT_PREV(Okular::FromBottom, true, false); + TEST_NEXT_PREV(Okular::PreviousResult, true, false); + TEST_NEXT_PREV(Okular::NextResult, true, false); + TEST_NEXT_PREV(Okular::PreviousResult, true, false); + TEST_NEXT_PREV(Okular::PreviousResult, false, false); delete page; } @@ -178,7 +180,7 @@ d.openDocument(testFile, QUrl(), mime); const int searchId = 0; - d.searchText(searchId, QStringLiteral(" i "), true, Qt::CaseSensitive, Okular::Document::NextMatch, false, QColor()); + d.searchText(searchId, QStringLiteral(" i "), true, Qt::CaseSensitive, Okular::Document::NextMatch, false, QColor(), false); QTRY_COMPARE(spy.count(), 1); QCOMPARE(receiver.m_id, searchId); QCOMPARE(receiver.m_status, Okular::Document::MatchFound); @@ -200,7 +202,7 @@ CREATE_PAGE; - Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("a"), Okular::FromBottom, Qt::CaseSensitive, nullptr); + Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("a"), Okular::FromBottom, Qt::CaseSensitive, nullptr, false); QVERIFY(result); delete result; @@ -219,7 +221,7 @@ CREATE_PAGE; - Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("ab"), Okular::FromTop, Qt::CaseSensitive, nullptr); + Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("ab"), Okular::FromTop, Qt::CaseSensitive, nullptr, false); QVERIFY(result); Okular::RegularAreaRect expected; expected.append(rect[1]); @@ -250,7 +252,7 @@ CREATE_PAGE; - Okular::RegularAreaRect* result = tp->findText(0, QString::fromUtf8("İ"), Okular::FromTop, Qt::CaseInsensitive, nullptr); + Okular::RegularAreaRect* result = tp->findText(0, QString::fromUtf8("İ"), Okular::FromTop, Qt::CaseInsensitive, nullptr, false); QVERIFY(result); delete result; @@ -276,7 +278,7 @@ CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("supercalifragilisticexpialidocious"), - Okular::FromTop, Qt::CaseSensitive, nullptr); + Okular::FromTop, Qt::CaseSensitive, nullptr, false); QVERIFY(result); Okular::RegularAreaRect expected; for (int i = 0; i < text.size(); i++) { @@ -290,11 +292,11 @@ } #define CREATE_PAGE_AND_TEST_SEARCH(searchString, matchExpected) \ -{ \ +// { \ CREATE_PAGE; \ \ Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral(searchString), \ - Okular::FromTop, Qt::CaseSensitive, NULL); \ + Okular::FromTop, Qt::CaseSensitive, NULL, false); \ \ QCOMPARE(!!result, matchExpected); \ \ @@ -349,21 +351,82 @@ { Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("a"), - Okular::FromTop, Qt::CaseSensitive, nullptr); + Okular::FromTop, Qt::CaseSensitive, nullptr, false); QVERIFY(result); delete result; } { Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("a"), - Okular::FromBottom, Qt::CaseSensitive, nullptr); + Okular::FromBottom, Qt::CaseSensitive, nullptr, false); QVERIFY(result); delete result; } delete page; } +void SearchTest::testWholeWords_data() +{ + //Tests if we are encountering whole words. + //The first test searchs for b, should encounter only one ocurrence. + //The second test searchs for ab, and should encounter the first two results + //The third test is looking for ab, it will find one result. + //The fourth test is looking for a and it finds one ocurrence + #define TEST_NEXT_AND_PREV 0 + #define TEST_NEXT_PREV_NEXT 1 + QTest::addColumn>("text"); + QTest::addColumn("searchString"); + QTest::addColumn("testNumber"); + + QTest::newRow("b find one normally") << (QVector() << QStringLiteral("b") << QStringLiteral("a") << QStringLiteral(" ") << QStringLiteral("b") << QStringLiteral("c") << QStringLiteral(" ") << QStringLiteral("b")) << QStringLiteral("b") << TEST_NEXT_AND_PREV; + QTest::newRow("ab find two with dots") << (QVector() << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral(".") << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral(".") << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("c")) << QStringLiteral("ab") << TEST_NEXT_PREV_NEXT; + QTest::newRow("ab find with hyphen at end of page") << (QVector() << QStringLiteral("a") << QStringLiteral("-\n") << QStringLiteral("b") << QStringLiteral(" ") << QStringLiteral("a") << QStringLiteral("-") << QStringLiteral("b")) << QStringLiteral("ab") << TEST_NEXT_AND_PREV; + QTest::newRow("a find at end of page") << (QVector() << QStringLiteral("a\n") << QStringLiteral("a") << QStringLiteral("b\n")) << QStringLiteral("a") << TEST_NEXT_AND_PREV; + QTest::newRow("find same TinyTextEntity") << (QVector() << QStringLiteral("it") << QStringLiteral(" should find this ")) << QStringLiteral("find") << TEST_NEXT_AND_PREV; + QTest::newRow("find two TinyTextEntity") << (QVector() << QStringLiteral("find one then fi") << QStringLiteral("nd another")) << QStringLiteral("find") << TEST_NEXT_PREV_NEXT; + QTest::newRow("find on last TinyTextEntity") << (QVector() << QStringLiteral("ab ab\n")) << QStringLiteral("ab") << TEST_NEXT_PREV_NEXT; + QTest::newRow("find with \\n") << (QVector() << QStringLiteral("ab-\nab\n")) << QStringLiteral("ab") << TEST_NEXT_PREV_NEXT; + QTest::newRow("test find once") << (QVector() << QStringLiteral("it") << QStringLiteral(" should find this afind")) << QStringLiteral("find") << TEST_NEXT_AND_PREV; + QTest::newRow("test find and with \n") << (QVector() << QStringLiteral("it") << QStringLiteral(" should find this but not thisx\n")) << QStringLiteral("this") << TEST_NEXT_AND_PREV; + +} + +void SearchTest::testWholeWords() +{ + QFETCH(QVector, text); + QFETCH(QString, searchString); + QFETCH(int, testNumber); + + QVector rect; \ + + for (int i = 0; i < text.size(); i++) { + rect << Okular::NormalizedRect(0.1*i, 0.0, 0.1*(i+1), 0.1); \ + } + + CREATE_PAGE; + switch(testNumber) + { + case 0: + TEST_NEXT_PREV(Okular::FromTop, true, true); + TEST_NEXT_PREV(Okular::NextResult, false, true); + TEST_NEXT_PREV(Okular::FromBottom, true, true); + TEST_NEXT_PREV(Okular::PreviousResult, false, true); + break; + case 1: + TEST_NEXT_PREV(Okular::FromTop, true, true); + TEST_NEXT_PREV(Okular::NextResult, true, true); + TEST_NEXT_PREV(Okular::PreviousResult, true, true); + TEST_NEXT_PREV(Okular::NextResult, true, true); + TEST_NEXT_PREV(Okular::NextResult, false, true); + break; + default: + break; + } + delete page; + +} + void SearchTest::testOneColumn() { //Tests that the layout analysis algorithm does not create too many columns. @@ -388,7 +451,7 @@ CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("Only one column"), - Okular::FromTop, Qt::CaseSensitive, nullptr); + Okular::FromTop, Qt::CaseSensitive, nullptr, false); QVERIFY(result); delete result; @@ -416,7 +479,7 @@ CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("This text in"), - Okular::FromTop, Qt::CaseSensitive, nullptr); + Okular::FromTop, Qt::CaseSensitive, nullptr, false); QVERIFY(!result); delete result; diff --git a/conf/okular.kcfg b/conf/okular.kcfg --- a/conf/okular.kcfg +++ b/conf/okular.kcfg @@ -306,6 +306,9 @@ true + + false + diff --git a/core/document.h b/core/document.h --- a/core/document.h +++ b/core/document.h @@ -636,7 +636,13 @@ * @param type The type of the search. @ref SearchType * @param moveViewport Whether the viewport shall be moved to the position of the matches. * @param color The highlighting color of the matches. + * @param wholeWords Whether the search should only find whole words or not. + * @since 1.7 (KDE 18.12) */ + void searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, + SearchType type, bool moveViewport, const QColor & color, bool wholeWords ); + + // TODO: To be merged with wholeWords = false void searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor & color ); diff --git a/core/document.cpp b/core/document.cpp --- a/core/document.cpp +++ b/core/document.cpp @@ -135,6 +135,7 @@ Document::SearchType cachedType; Qt::CaseSensitivity cachedCaseSensitivity; bool cachedViewportMove : 1; + bool cachedWholeWords; bool isCurrentlySearching : 1; QColor cachedColor; int pagesDone; @@ -1669,7 +1670,7 @@ m_parent->requestTextPage( page->number() ); // if found a match on the current page, end the loop - searchStruct->match = page->findText( searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity ); + searchStruct->match = page->findText( searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity, nullptr, search->cachedWholeWords ); if ( !searchStruct->match ) { if (forward) searchStruct->currentPage++; @@ -3770,7 +3771,7 @@ } void Document::searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, - SearchType type, bool moveViewport, const QColor & color ) + SearchType type, bool moveViewport, const QColor & color, bool wholeWords ) { d->m_searchCancelled = false; @@ -3798,6 +3799,7 @@ s->cachedCaseSensitivity = caseSensitivity; s->cachedViewportMove = moveViewport; s->cachedColor = color; + s->cachedWholeWords = wholeWords; s->isCurrentlySearching = true; // global data for search @@ -3837,9 +3839,9 @@ if ( lastPage && lastPage->number() == s->continueOnPage ) { if ( newText ) - match = lastPage->findText( searchID, text, forward ? FromTop : FromBottom, caseSensitivity ); + match = lastPage->findText( searchID, text, forward ? FromTop : FromBottom, caseSensitivity, nullptr, wholeWords ); else - match = lastPage->findText( searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch ); + match = lastPage->findText( searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch, wholeWords ); if ( !match ) { if (forward) currentPage++; @@ -3869,6 +3871,12 @@ } } +void Document::searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, + SearchType type, bool moveViewport, const QColor & color ) +{ + searchText(searchID, text, fromStart, caseSensitivity, type, moveViewport, color, false ); +} + void Document::continueSearch( int searchID ) { // check if searchID is present in runningSearches @@ -3883,7 +3891,7 @@ RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, - p->cachedType, p->cachedViewportMove, p->cachedColor ); + p->cachedType, p->cachedViewportMove, p->cachedColor, p->cachedWholeWords ); } void Document::continueSearch( int searchID, SearchType type ) @@ -3900,7 +3908,7 @@ RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, - type, p->cachedViewportMove, p->cachedColor ); + type, p->cachedViewportMove, p->cachedColor, p->cachedWholeWords ); } void Document::resetSearch( int searchID ) diff --git a/core/page.h b/core/page.h --- a/core/page.h +++ b/core/page.h @@ -184,9 +184,16 @@ * the search is case insensitive. * @param lastRect If 0 (default) the search starts at the beginning of the page, otherwise * right/below the coordinates of the given rect. + * @param wholeWords If true, it will check if the word matched is whole. If it isn't, it + * continues the search. + * @since 1.7 (KDE 18.12) */ RegularAreaRect* findText( int id, const QString & text, SearchDirection direction, - Qt::CaseSensitivity caseSensitivity, const RegularAreaRect * lastRect=nullptr) const; + Qt::CaseSensitivity caseSensitivity, const RegularAreaRect * lastRect, + const bool wholeWords) const; + // TODO: To be merged with wholeWords = false, not forget to initialize lastRect with nullptr + RegularAreaRect* findText( int id, const QString & text, SearchDirection direction, + Qt::CaseSensitivity caseSensitivity, const RegularAreaRect * lastRect=nullptr ) const; /** * Returns the page text (or part of it). diff --git a/core/page.cpp b/core/page.cpp --- a/core/page.cpp +++ b/core/page.cpp @@ -301,16 +301,22 @@ } RegularAreaRect * Page::findText( int id, const QString & text, SearchDirection direction, - Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect ) const + Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect, + const bool wholeWords ) const { RegularAreaRect* rect = nullptr; if ( text.isEmpty() || !d->m_text ) return rect; - - rect = d->m_text->findText( id, text, direction, caseSensitivity, lastRect ); + rect = d->m_text->findText( id, text, direction, caseSensitivity, lastRect, wholeWords ); return rect; } +RegularAreaRect * Page::findText( int id, const QString & text, SearchDirection direction, + Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect ) const +{ + return findText( id, text, direction, caseSensitivity, lastRect, false); +} + QString Page::text( const RegularAreaRect * area ) const { return text( area, TextPage::AnyPixelTextAreaInclusionBehaviour ); diff --git a/core/textpage.h b/core/textpage.h --- a/core/textpage.h +++ b/core/textpage.h @@ -137,7 +137,15 @@ * the search is case insensitive. * @param lastRect If 0 the search starts at the beginning of the page, otherwise * right/below the coordinates of the given rect. + * @param wholeWords If true, it will check if the word matched is whole. If it isn't, it + * continues the search. + * @since 1.7 (KDE 18.12) */ + RegularAreaRect* findText( int id, const QString &text, SearchDirection direction, + Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect, + const bool wholeWords ); + + // TODO: To merge with wholeWords = false RegularAreaRect* findText( int id, const QString &text, SearchDirection direction, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect ); diff --git a/core/textpage.cpp b/core/textpage.cpp --- a/core/textpage.cpp +++ b/core/textpage.cpp @@ -714,7 +714,8 @@ RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchDirection direct, - Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *area ) + Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *area, + const bool wholeWords ) { SearchDirection dir=direct; // invalid search request @@ -764,14 +765,19 @@ ? CaseSensitiveCmpFn : CaseInsensitiveCmpFn; if ( forward ) { - ret = d->findTextInternalForward( searchID, query, cmpFn, start, start_offset, end ); + ret = d->findTextInternalForward( searchID, query, cmpFn, start, start_offset, end, wholeWords ); } else { - ret = d->findTextInternalBackward( searchID, query, cmpFn, start, start_offset, end ); + ret = d->findTextInternalBackward( searchID, query, cmpFn, start, start_offset, end, wholeWords ); } return ret; } +RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchDirection direct, + Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *area ) +{ + return findText( searchID, query, direct, caseSensitivity, area, false); +} // hyphenated '-' must be at the end of a word, so hyphenation means // we have a '-' just followed by a '\n' character @@ -839,19 +845,103 @@ return ret; } +// Checks if the word searched is whole + +const bool TextPagePrivate::isWholeWord( const TextList::ConstIterator &start, + const TextList::ConstIterator &end, + int start_offset, int end_offset) +{ + // Divided in two parts, after the word and before + bool part1 = false, part2 = false; + + // If I'm at the beginning/end of the text, I don't need to check after/before + if( start == m_words.constBegin() && start_offset == 0) + { + part1 = true; + } + + else + { + // I have to check if it is the same textEntity + TextList::ConstIterator before = start; + if(start_offset != 0){ + const TinyTextEntity* curEntity = *before; + const QString& str = curEntity->text(); + if( !str[start_offset-1].isLetterOrNumber() ) + { + part1 = true; + } + } + // Else I check if the text before is a number or letter, if it isn't, a word begins here + else{ + before--; + const TinyTextEntity* curEntity = *before; + const QString& str = curEntity->text(); + const int len = stringLengthAdaptedWithHyphen( str, before, m_words.constEnd() ); + if( len != 0 && !str[len-1].isLetterOrNumber() ) + { + part1 = true; + } + } + } + + // For the end is a bit more complicated, some text entities can end at a \n, + // and checking the next would get the first char in the next phrase. + // So its checking this right here. + TextList::ConstIterator after = end; + const TinyTextEntity* endEntity = *after; + const QString& endStr = endEntity->text(); + + const int realEndStrSize = stringLengthAdaptedWithHyphen( endStr, after, m_words.constEnd() ); + + // I have to check if it's the same textEntity too, also check if ends with '\n' and if it does, it needs to be the last + // char before \n + if( (endStr.endsWith( QStringLiteral("\n")) && realEndStrSize == endStr.size() && end_offset == endStr.size() - 1) + || (end_offset != endStr.size() && !endStr[end_offset].isLetterOrNumber()) ) + { + part2 = true; + } + + // If it doesn't end in \n, I check the next char, as done in before. + else if( end_offset == endStr.size() ) + { + after++; + // Have to check if this is the end, because the end has no chars. + if( after == m_words.constEnd() ) + { + part2 = true; + } + + else + { + const TinyTextEntity* curEntity = *after; + const QString& str = curEntity->text(); + // Checking if the first char on the entity is a letter or number + // Also check if it ends on -\n, if it does, should not match. + if( !str[0].isLetterOrNumber() && !str.endsWith( (QStringLiteral("-\n")))) + { + part2 = true; + } + } + } + + return part1 && part2; +} + RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, - const TextList::ConstIterator &end) + const TextList::ConstIterator &end, + bool wholeWords ) { // normalize query search all unicode (including glyphs) const QString query = _query.normalized(QString::NormalizationForm_KC); // j is the current position in our query // len is the length of the string in TextEntity // queryLeft is the length of the query we have left - int j=0, queryLeft=query.length(); + int j = 0, queryLeft = query.length(); TextList::ConstIterator it = start; int offset = start_offset; @@ -878,7 +968,7 @@ offset_begin = offset; } - int min=qMin(queryLeft,len-offset); + int min = qMin(queryLeft,len-offset); { #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << str.midRef(offset, min) << ":" << _query.midRef(j, min); @@ -896,9 +986,9 @@ qCDebug(OkularCoreDebug) << "\tnot matched"; #endif j = 0; - queryLeft=query.length(); + queryLeft = query.length(); it = it_begin; - offset = offset_begin+1; + offset = offset_begin + 1; it_begin = TextList::ConstIterator(); } else @@ -917,18 +1007,34 @@ if (queryLeft==0) { - // save or update the search point for the current searchID - QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); - if ( sIt == m_searchPoints.end() ) + + //If it's looking for whole words and this is not a whole word + if(wholeWords && !isWholeWord(it_begin, it, offset_begin, offset + min)) + { + j = 0; + queryLeft = query.length(); + it = it_begin; + offset = offset_begin + 1; + it_begin = TextList::ConstIterator(); + continue; + } + + else { - sIt = m_searchPoints.insert( searchID, new SearchPoint ); + // save or update the search point for the current searchID + QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); + if ( sIt == m_searchPoints.end() ) + { + sIt = m_searchPoints.insert( searchID, new SearchPoint ); + } + SearchPoint* sp = *sIt; + sp->it_begin = it_begin; + sp->it_end = it; + sp->offset_begin = offset_begin; + sp->offset_end = offset + min; + return searchPointToArea(sp); } - SearchPoint* sp = *sIt; - sp->it_begin = it_begin; - sp->it_end = it; - sp->offset_begin = offset_begin; - sp->offset_end = offset + min; - return searchPointToArea(sp); + } it++; @@ -948,19 +1054,29 @@ return nullptr; } +RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const QString &_query, + TextComparisonFunction comparer, + const TextList::ConstIterator &start, + int start_offset, + const TextList::ConstIterator &end ) +{ + return findTextInternalForward(searchID, _query, comparer, start, start_offset, end, false); +} + RegularAreaRect* TextPagePrivate::findTextInternalBackward( int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, - const TextList::ConstIterator &end) + const TextList::ConstIterator &end, + bool wholeWords ) { // normalize query to search all unicode (including glyphs) const QString query = _query.normalized(QString::NormalizationForm_KC); // j is the current position in our query // len is the length of the string in TextEntity // queryLeft is the length of the query we have left - int j=query.length(), queryLeft=query.length(); + int j = query.length(), queryLeft = query.length(); TextList::ConstIterator it = start; int offset = start_offset; @@ -994,7 +1110,7 @@ offset_begin = offset; } - int min=qMin(queryLeft,offset); + int min = qMin(queryLeft,offset); { #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << str.midRef(offset-min, min) << " : " << _query.midRef(j-min, min); @@ -1016,7 +1132,7 @@ j = query.length(); queryLeft = query.length(); it = it_begin; - offset = offset_begin-1; + offset = offset_begin - 1; it_begin = TextList::ConstIterator(); } else @@ -1035,18 +1151,32 @@ if ( queryLeft == 0 ) { - // save or update the search point for the current searchID - QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); - if ( sIt == m_searchPoints.end() ) + + //If it's looking for whole words and this is not a whole word + if(wholeWords && !isWholeWord(it, it_begin, offset-min, offset_begin)) + { + j = query.length(); + queryLeft = query.length(); + it = it_begin; + offset = offset_begin - 1; + it_begin = TextList::ConstIterator(); + continue; + } + else { - sIt = m_searchPoints.insert( searchID, new SearchPoint ); + // save or update the search point for the current searchID + QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); + if ( sIt == m_searchPoints.end() ) + { + sIt = m_searchPoints.insert( searchID, new SearchPoint ); + } + SearchPoint* sp = *sIt; + sp->it_begin = it; + sp->it_end = it_begin; + sp->offset_begin = offset - min; + sp->offset_end = offset_begin; + return searchPointToArea(sp); } - SearchPoint* sp = *sIt; - sp->it_begin = it; - sp->it_end = it_begin; - sp->offset_begin = offset - min; - sp->offset_end = offset_begin; - return searchPointToArea(sp); } offset = 0; @@ -1067,6 +1197,15 @@ return nullptr; } +RegularAreaRect * TextPagePrivate::findTextInternalBackward( int searchID, const QString &_query, + TextComparisonFunction comparer, + const TextList::ConstIterator &start, + int start_offset, + const TextList::ConstIterator &end ) +{ + return findTextInternalBackward( searchID, _query, comparer, start, start_offset, end, false ); +} + QString TextPage::text(const RegularAreaRect *area) const { return text(area, AnyPixelTextAreaInclusionBehaviour); diff --git a/core/textpage_p.h b/core/textpage_p.h --- a/core/textpage_p.h +++ b/core/textpage_p.h @@ -42,12 +42,30 @@ public: TextPagePrivate(); ~TextPagePrivate(); + + // @since 1.7 (KDE 18.12) + RegularAreaRect * findTextInternalForward( int searchID, const QString &query, + TextComparisonFunction comparer, + const TextList::ConstIterator &start, + int start_offset, + const TextList::ConstIterator &end, + bool wholeWords ); + // TODO: to be merged with wholeWords = false RegularAreaRect * findTextInternalForward( int searchID, const QString &query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, - const TextList::ConstIterator &end); + const TextList::ConstIterator &end ); + + // @since 1.7 (KDE 18.12) + RegularAreaRect * findTextInternalBackward( int searchID, const QString &query, + TextComparisonFunction comparer, + const TextList::ConstIterator &start, + int start_offset, + const TextList::ConstIterator &end, + bool wholeWords ); + // TODO: to be merged with wholeWords = false RegularAreaRect * findTextInternalBackward( int searchID, const QString &query, TextComparisonFunction comparer, const TextList::ConstIterator &start, @@ -59,6 +77,16 @@ */ void setWordList(const TextList &list); + /* + * Checks if the word between the pointers start and end is a whole word. + * A whole word is determined by a word which has no alphanumeric chars behind + * or after its chars. + * @since 1.7 (KDE 18.12) + */ + const bool isWholeWord( const TextList::ConstIterator &start, + const TextList::ConstIterator &end, + int start_offset, int end_offset); + /** * Make necessary modifications in the TextList to make the text order correct, so * that textselection works fine diff --git a/mobile/components/documentitem.cpp b/mobile/components/documentitem.cpp --- a/mobile/components/documentitem.cpp +++ b/mobile/components/documentitem.cpp @@ -181,7 +181,7 @@ m_document->cancelSearch(); m_document->resetSearch(PAGEVIEW_SEARCH_ID); m_document->searchText(PAGEVIEW_SEARCH_ID, text, 1, Qt::CaseInsensitive, - Okular::Document::AllDocument, true, QColor(100,100,200,40)); + Okular::Document::AllDocument, true, QColor(100,100,200,40), false); if (!m_searchInProgress) { m_searchInProgress = true; diff --git a/ui/findbar.h b/ui/findbar.h --- a/ui/findbar.h +++ b/ui/findbar.h @@ -46,6 +46,7 @@ private Q_SLOTS: void caseSensitivityChanged(); + void findWholeWordsChanged(); void fromCurrentPageChanged(); void findAsYouTypeChanged(); void closeAndStopSearch(); @@ -55,6 +56,7 @@ QAction * m_caseSensitiveAct; QAction * m_fromCurrentPageAct; QAction * m_findAsYouTypeAct; + QAction * m_findWholeWords; bool eventFilter( QObject *target, QEvent *event ) override; bool m_active; }; diff --git a/ui/findbar.cpp b/ui/findbar.cpp --- a/ui/findbar.cpp +++ b/ui/findbar.cpp @@ -71,6 +71,9 @@ m_fromCurrentPageAct->setCheckable( true ); m_findAsYouTypeAct = optionsMenu->addAction( i18n( "Find as you type" ) ); m_findAsYouTypeAct->setCheckable( true ); + m_findWholeWords = optionsMenu->addAction( i18n( "Find whole words only" ) ); + m_findWholeWords->setCheckable( true ); + optionsBtn->setMenu( optionsMenu ); lay->addWidget( optionsBtn ); @@ -80,10 +83,12 @@ connect( m_caseSensitiveAct, &QAction::toggled, this, &FindBar::caseSensitivityChanged ); connect( m_fromCurrentPageAct, &QAction::toggled, this, &FindBar::fromCurrentPageChanged ); connect( m_findAsYouTypeAct, &QAction::toggled, this, &FindBar::findAsYouTypeChanged ); + connect( m_findWholeWords, &QAction::toggled, this, &FindBar::findWholeWordsChanged ); m_caseSensitiveAct->setChecked( Okular::Settings::searchCaseSensitive() ); m_fromCurrentPageAct->setChecked( Okular::Settings::searchFromCurrentPage() ); m_findAsYouTypeAct->setChecked( Okular::Settings::findAsYouType() ); + m_findWholeWords->setChecked( Okular::Settings::findWholeWords() ); hide(); @@ -169,6 +174,16 @@ m_search->lineEdit()->restartSearch(); } +void FindBar::findWholeWordsChanged() +{ + m_search->lineEdit()->setWholeWords( m_findWholeWords->isChecked() ); + if( !m_active ) + return; + Okular::Settings::setFindWholeWords( m_findWholeWords->isChecked() ); + Okular::Settings::self()->save(); + m_search->lineEdit()->restartSearch(); +} + void FindBar::fromCurrentPageChanged() { m_search->lineEdit()->setSearchFromStart( !m_fromCurrentPageAct->isChecked() ); diff --git a/ui/searchlineedit.h b/ui/searchlineedit.h --- a/ui/searchlineedit.h +++ b/ui/searchlineedit.h @@ -30,6 +30,7 @@ void clearText(); void setSearchCaseSensitivity( Qt::CaseSensitivity cs ); + void setWholeWords( bool wholeWords ); void setSearchMinimumLength( int length ); void setSearchType( Okular::Document::SearchType type ); void setSearchId( int id ); @@ -62,6 +63,7 @@ int m_id; QColor m_color; bool m_moveViewport; + bool m_wholeWords; bool m_changed; bool m_fromStart; bool m_findAsYouType; diff --git a/ui/searchlineedit.cpp b/ui/searchlineedit.cpp --- a/ui/searchlineedit.cpp +++ b/ui/searchlineedit.cpp @@ -29,7 +29,7 @@ m_caseSensitivity( Qt::CaseInsensitive ), m_searchType( Okular::Document::AllDocument ), m_id( -1 ), m_moveViewport( false ), m_changed( false ), m_fromStart( true ), - m_findAsYouType( true ), m_searchRunning( false ) + m_findAsYouType( true ), m_searchRunning( false ), m_wholeWords( false ) { setObjectName( QStringLiteral( "SearchLineEdit" ) ); setClearButtonEnabled( true ); @@ -54,6 +54,13 @@ m_changed = true; } + +void SearchLineEdit::setWholeWords( bool wholeWords ) +{ + m_wholeWords = wholeWords; + m_changed = true; +} + void SearchLineEdit::setSearchMinimumLength( int length ) { m_minLength = length; @@ -242,7 +249,7 @@ emit searchStarted(); m_searchRunning = true; m_document->searchText( m_id, thistext, m_fromStart, m_caseSensitivity, - m_searchType, m_moveViewport, m_color ); + m_searchType, m_moveViewport, m_color, m_wholeWords ); } else m_document->resetSearch( m_id );