diff --git a/core/textpage.cpp b/core/textpage.cpp index 855942dff..c1bd1f2be 100644 --- a/core/textpage.cpp +++ b/core/textpage.cpp @@ -33,27 +33,32 @@ class SearchPoint { } + /** The TinyTextEntity containing the first character of the match. */ TextList::ConstIterator it_begin; + + /** The TinyTextEntity containing the last character of the match. */ TextList::ConstIterator it_end; + + /** The index of the first character of the match in (*it_begin)->text(). + * Satisfies 0 <= offset_begin < (*it_begin)->text().length(). + */ int offset_begin; + + /** One plus the index of the last character of the match in (*it_end)->text(). + * Satisfies 0 < offset_end <= (*it_end)->text().length(). + */ int offset_end; }; /* text comparison functions */ -bool CaseInsensitiveCmpFn( const QStringRef & from, const QStringRef & to, - int *fromLength, int *toLength ) +static bool CaseInsensitiveCmpFn( const QStringRef & from, const QStringRef & to ) { - *fromLength = from.length(); - *toLength = to.length(); return from.compare( to, Qt::CaseInsensitive ) == 0; } -bool CaseSensitiveCmpFn( const QStringRef & from, const QStringRef & to, - int *fromLength, int *toLength ) +static bool CaseSensitiveCmpFn( const QStringRef & from, const QStringRef & to ) { - *fromLength = from.length(); - *toLength = to.length(); return from.compare( to, Qt::CaseSensitive ) == 0; } @@ -106,7 +111,7 @@ static bool doesConsumeY(const QRect& first, const QRect& second, int threshold) const int overlap = (second.bottom() >= first.bottom()) ? first.bottom() - second.top() : second.bottom() - first.top(); //we will divide by the smaller rectangle to calculate the overlap - const int percentage = (first.width() < second.width()) ? overlap * 100 / (first.bottom() - first.top()) + const int percentage = (first.height() < second.height()) ? overlap * 100 / (first.bottom() - first.top()) : overlap * 100 / (second.bottom() - second.top()); if(percentage >= threshold) return true; @@ -730,6 +735,7 @@ RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchD if ( d->m_words.isEmpty() || query.isEmpty() || ( area && area->isNull() ) ) return 0; TextList::ConstIterator start; + int start_offset = 0; TextList::ConstIterator end; const QMap< int, SearchPoint* >::const_iterator sIt = d->m_searchPoints.constFind( searchID ); if ( sIt == d->m_searchPoints.constEnd() ) @@ -746,28 +752,24 @@ RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchD { case FromTop: start = d->m_words.constBegin(); + start_offset = 0; end = d->m_words.constEnd(); break; case FromBottom: start = d->m_words.constEnd(); + start_offset = 0; end = d->m_words.constBegin(); - Q_ASSERT( start != end ); - // we can safely go one step back, as we already checked - // that the list is not empty - --start; forward = false; break; case NextResult: start = (*sIt)->it_end; + start_offset = (*sIt)->offset_end; end = d->m_words.constEnd(); - if ( ( start + 1 ) != end ) - ++start; break; case PreviousResult: start = (*sIt)->it_begin; + start_offset = (*sIt)->offset_begin; end = d->m_words.constBegin(); - if ( start != end ) - --start; forward = false; break; }; @@ -776,11 +778,11 @@ RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchD ? CaseSensitiveCmpFn : CaseInsensitiveCmpFn; if ( forward ) { - ret = d->findTextInternalForward( searchID, query, caseSensitivity, cmpFn, start, end ); + ret = d->findTextInternalForward( searchID, query, cmpFn, start, start_offset, end ); } else { - ret = d->findTextInternalBackward( searchID, query, caseSensitivity, cmpFn, start, end ); + ret = d->findTextInternalBackward( searchID, query, cmpFn, start, start_offset, end ); } return ret; } @@ -789,7 +791,7 @@ RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchD // we have a '-' just followed by a '\n' character // check if the string contains a '-' character // if the '-' is the last entry -static int stringLengthAdaptedWithHyphen(const QString &str, const TextList::ConstIterator &it, const TextList::ConstIterator &end, PagePrivate *page) +static int stringLengthAdaptedWithHyphen(const QString &str, const TextList::ConstIterator &it, const TextList::ConstIterator &textListEnd, PagePrivate *page) { int len = str.length(); @@ -800,7 +802,7 @@ static int stringLengthAdaptedWithHyphen(const QString &str, const TextList::Con if ( str.endsWith( '-' ) ) { // validity chek of it + 1 - if ( ( it + 1 ) != end ) + if ( ( it + 1 ) != textListEnd ) { // 1. if the next character is '\n' const QString &lookahedStr = (*(it+1))->text(); @@ -834,68 +836,85 @@ static int stringLengthAdaptedWithHyphen(const QString &str, const TextList::Con return len; } -RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const QString &_query, - Qt::CaseSensitivity caseSensitivity, - TextComparisonFunction comparer, - const TextList::ConstIterator &start, - const TextList::ConstIterator &end ) +RegularAreaRect* TextPagePrivate::searchPointToArea(const SearchPoint* sp) { const QTransform matrix = m_page ? m_page->rotationMatrix() : QTransform(); - RegularAreaRect* ret=new RegularAreaRect; + for (TextList::ConstIterator it = sp->it_begin; ; it++) + { + const TinyTextEntity* curEntity = *it; + ret->append( curEntity->transformedArea( matrix ) ); + + if (it == sp->it_end) { + break; + } + } + + ret->simplify(); + return ret; +} + +RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const QString &_query, + TextComparisonFunction comparer, + const TextList::ConstIterator &start, + int start_offset, + const TextList::ConstIterator &end) +{ // normalize query search all unicode (including glyphs) - const QString query = (caseSensitivity == Qt::CaseSensitive) - ? _query.normalized(QString::NormalizationForm_KC) - : _query.toLower().normalized(QString::NormalizationForm_KC); + 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 - const TinyTextEntity* curEntity = 0; - int j=0, len=0, queryLeft=query.length(); - int offset = 0; - bool haveMatch=false; - bool offsetMoved = false; + int j=0, queryLeft=query.length(); + TextList::ConstIterator it = start; - TextList::ConstIterator it_begin; - for ( ; it != end; ++it ) + int offset = start_offset; + + TextList::ConstIterator it_begin = TextList::ConstIterator(); + int offset_begin = 0; //dummy initial value to suppress compiler warnings + + while ( it != end ) { - curEntity = *it; - const QString &str = curEntity->text(); - if ( !offsetMoved && ( it == start ) ) + const TinyTextEntity* curEntity = *it; + const QString& str = curEntity->text(); + int len = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd(), m_page); + + if (offset >= len) { - if ( m_searchPoints.contains( searchID ) ) - { - offset = qMax( m_searchPoints[ searchID ]->offset_end, 0 ); - } - offsetMoved = true; + it++; + offset = 0; + continue; } + + if ( it_begin == TextList::ConstIterator() ) + { + it_begin = it; + offset_begin = offset; + } + + int min=qMin(queryLeft,len-offset); { - len = stringLengthAdaptedWithHyphen(str, it, end, m_page); - int min=qMin(queryLeft,len); #ifdef DEBUG_TEXTPAGE - kDebug(OkularDebug) << str.mid(offset,min) << ":" << _query.mid(j,min); + kDebug(OkularDebug) << str.midRef(offset, min) << ":" << _query.midRef(j, min); #endif // we have equal (or less than) area of the query left as the length of the current // entity - int resStrLen = 0, resQueryLen = 0; - if ( !comparer( str.midRef( offset, min ), query.midRef( j, min ), - &resStrLen, &resQueryLen ) ) + if ( !comparer( str.midRef( offset, min ), query.midRef( j, min ) ) ) { - // we not have matched + // we have not matched // this means we do not have a complete match // we need to get back to query start // and continue the search from this place - haveMatch=false; - ret->clear(); #ifdef DEBUG_TEXTPAGE kDebug(OkularDebug) << "\tnot matched"; #endif - j=0; - offset = 0; + j = 0; queryLeft=query.length(); + it = it_begin; + offset = offset_begin+1; it_begin = TextList::ConstIterator(); } else @@ -909,35 +928,32 @@ RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const Q #ifdef DEBUG_TEXTPAGE kDebug(OkularDebug) << "\tmatched"; #endif - haveMatch=true; - ret->append( curEntity->transformedArea( matrix ) ); - j += resStrLen; - queryLeft -= resQueryLen; - if ( it_begin == TextList::ConstIterator() ) + j += min; + queryLeft -= min; + + if (queryLeft==0) { - it_begin = it; + // 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); } - } - } - if (haveMatch && queryLeft==0 && j==query.length()) - { - // 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 ); + it++; + offset = 0; } - SearchPoint* sp = *sIt; - sp->it_begin = it_begin; - sp->it_end = it - 1; - sp->offset_begin = j; - sp->offset_end = j + qMin( queryLeft, len ); - ret->simplify(); - return ret; } } // end of loop - it means that we've ended the textentities + const QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt != m_searchPoints.end() ) { @@ -945,68 +961,78 @@ RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const Q m_searchPoints.erase( sIt ); delete sp; } - delete ret; return 0; } RegularAreaRect* TextPagePrivate::findTextInternalBackward( int searchID, const QString &_query, - Qt::CaseSensitivity caseSensitivity, TextComparisonFunction comparer, const TextList::ConstIterator &start, - const TextList::ConstIterator &loop_end ) + int start_offset, + const TextList::ConstIterator &end) { - const QTransform matrix = m_page ? m_page->rotationMatrix() : QTransform(); - - RegularAreaRect* ret=new RegularAreaRect; - // normalize query to search all unicode (including glyphs) - const QString query = (caseSensitivity == Qt::CaseSensitive) - ? _query.normalized(QString::NormalizationForm_KC) - : _query.toLower().normalized(QString::NormalizationForm_KC); + 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 - const TinyTextEntity* curEntity = 0; - int j=query.length() - 1, len=0, queryLeft=query.length(); - bool haveMatch=false; - bool offsetMoved = false; + int j=query.length(), queryLeft=query.length(); + TextList::ConstIterator it = start; - TextList::ConstIterator it_begin; + int offset = start_offset; + + TextList::ConstIterator it_begin = TextList::ConstIterator(); + int offset_begin = 0; //dummy initial value to suppress compiler warnings + while ( true ) { - curEntity = *it; - const QString &str = curEntity->text(); - if ( !offsetMoved && ( it == start ) ) + if (offset <= 0) { - offsetMoved = true; + if ( it == end ) + { + break; + } + it--; } + + const TinyTextEntity* curEntity = *it; + const QString& str = curEntity->text(); + int len = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd(), m_page); + + if (offset <= 0) + { + offset = len; + } + + if ( it_begin == TextList::ConstIterator() ) + { + it_begin = it; + offset_begin = offset; + } + + int min=qMin(queryLeft,offset); { - len = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd(), m_page); - int min=qMin(queryLeft,len); #ifdef DEBUG_TEXTPAGE - kDebug(OkularDebug) << str.right(min) << " : " << _query.mid(j-min+1,min); + kDebug(OkularDebug) << str.midRef(offset-min, min) << " : " << _query.midRef(j-min, min); #endif // we have equal (or less than) area of the query left as the length of the current // entity - int resStrLen = 0, resQueryLen = 0; // Note len is not str.length() so we can't use rightRef here - const int offset = len - min; - if ( !comparer( str.midRef(offset, min ), query.midRef( j - min + 1, min ), - &resStrLen, &resQueryLen ) ) + if ( !comparer( str.midRef(offset-min, min ), query.midRef( j - min, min ) ) ) { - // we not have matched + // we have not matched // this means we do not have a complete match // we need to get back to query start // and continue the search from this place - haveMatch=false; - ret->clear(); #ifdef DEBUG_TEXTPAGE kDebug(OkularDebug) << "\tnot matched"; #endif - j=query.length() - 1; - queryLeft=query.length(); + + j = query.length(); + queryLeft = query.length(); + it = it_begin; + offset = offset_begin-1; it_begin = TextList::ConstIterator(); } else @@ -1020,39 +1046,33 @@ RegularAreaRect* TextPagePrivate::findTextInternalBackward( int searchID, const #ifdef DEBUG_TEXTPAGE kDebug(OkularDebug) << "\tmatched"; #endif - haveMatch=true; - ret->append( curEntity->transformedArea( matrix ) ); - j -= resStrLen; - queryLeft -= resQueryLen; - if ( it_begin == TextList::ConstIterator() ) + j -= min; + queryLeft -= min; + + if ( queryLeft == 0 ) { - it_begin = it; + // 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); } - } - } - if (haveMatch && queryLeft==0 && j<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() ) - { - sIt = m_searchPoints.insert( searchID, new SearchPoint ); + offset = 0; } - SearchPoint* sp = *sIt; - sp->it_begin = it; - sp->it_end = it_begin; - sp->offset_begin = j; - sp->offset_end = j + qMin( queryLeft, len ); - ret->simplify(); - return ret; + } - if ( it == loop_end ) - break; - else - --it; + } // end of loop - it means that we've ended the textentities + const QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt != m_searchPoints.end() ) { @@ -1060,7 +1080,6 @@ RegularAreaRect* TextPagePrivate::findTextInternalBackward( int searchID, const m_searchPoints.erase( sIt ); delete sp; } - delete ret; return 0; } diff --git a/core/textpage_p.h b/core/textpage_p.h index 8ecf0c907..486d5cb2b 100644 --- a/core/textpage_p.h +++ b/core/textpage_p.h @@ -26,8 +26,11 @@ namespace Okular class PagePrivate; typedef QList< TinyTextEntity* > TextList; -typedef bool ( *TextComparisonFunction )( const QStringRef & from, const QStringRef & to, - int *fromLength, int *toLength ); +/** + * Returns whether the two strings match. + * Satisfies the condition that if two strings match then their lengths are equal. + */ +typedef bool ( *TextComparisonFunction )( const QStringRef & from, const QStringRef & to ); /** * A list of RegionText. It keeps a bunch of TextList with their bounding rectangles @@ -41,14 +44,14 @@ class TextPagePrivate ~TextPagePrivate(); RegularAreaRect * findTextInternalForward( int searchID, const QString &query, - Qt::CaseSensitivity caseSensitivity, TextComparisonFunction comparer, const TextList::ConstIterator &start, - const TextList::ConstIterator &end ); + int start_offset, + const TextList::ConstIterator &end); RegularAreaRect * findTextInternalBackward( int searchID, const QString &query, - Qt::CaseSensitivity caseSensitivity, TextComparisonFunction comparer, const TextList::ConstIterator &start, + int start_offset, const TextList::ConstIterator &end ); /** @@ -66,6 +69,9 @@ class TextPagePrivate TextList m_words; QMap< int, SearchPoint* > m_searchPoints; PagePrivate *m_page; + + private: + RegularAreaRect * searchPointToArea(const SearchPoint* sp); }; } diff --git a/tests/searchtest.cpp b/tests/searchtest.cpp index 495107d3a..9b1dda5b0 100644 --- a/tests/searchtest.cpp +++ b/tests/searchtest.cpp @@ -10,6 +10,8 @@ #include #include "../core/document.h" +#include "../core/page.h" +#include "../core/textpage.h" #include "../settings_core.h" Q_DECLARE_METATYPE(Okular::Document::SearchStatus) @@ -36,17 +38,130 @@ class SearchTest : public QObject private slots: void initTestCase(); + void testNextAndPrevious(); void test311232(); + void test323262(); + void test323263(); + void testDottedI(); + void testHyphenAtEndOfLineWithoutYOverlap(); + void testHyphenWithYOverlap(); + void testHyphenAtEndOfPage(); }; void SearchTest::initTestCase() { qRegisterMetaType(); + Okular::SettingsCore::instance( "searchtest" ); +} + +static Okular::TextPage* createTextPage(const QString text[], const Okular::NormalizedRect rect[], + int n, Okular::Page*& page) { + + Okular::TextPage* tp = new Okular::TextPage(); + for (int i = 0; i < n; i++) { + tp->append(text[i], new Okular::NormalizedRect(rect[i])); + } + + //We must create a Page object because TextPage::stringLengthAdaptedWithHyphen uses + //m_page->width() and m_page->height() (for some dubious reason). + //Note that calling "delete page;" will delete the TextPage as well. + page = new Okular::Page(1, 100, 100, Okular::Rotation0); + page -> setTextPage(tp); + + return tp; +} + +#define TEST_NEXT_PREV(searchType, expectedStatus) \ +{ \ + Okular::RegularAreaRect* result = tp->findText(0, searchString, searchType, Qt::CaseSensitive, NULL); \ + QCOMPARE(!!result, expectedStatus); \ + delete result; \ +} + +//The test testNextAndPrevious checks that +//a) if one starts a new search, then the first or last match is found, depending on the search direction +// (2 cases: FromTop/FromBottom) +//b) if the last search has found a match, +// then clicking the "Next" button moves to the next occurrence an "Previous" to the previous one +// (if there is any). Altogether there are four combinations of the last search and new search +// direction: Next-Next, Previous-Previous, Next-Previous, Previous-Next; the first two combination +// have two subcases (the new search may give a match or not, so altogether 6 cases to test). +//This gives 8 cases altogether. By taking into account the cases where the last search has given no match, +//we would have 4 more cases (Next (no match)-Next, Next (no match)-Previous, Previous (no match)-Previous, +//Previous (no match)-Next), but those are more the business of Okular::Document::searchText rather than +//Okular::TextPage (at least in the multi-page case). + +// We have four test situations: four documents and four corresponding search strings. +// The first situation (document="ababa", search string="b") is a generic one where the +//two matches are not side-by-side and neither the first character nor the last character of +//the document match. The only special thing is that the search string has only length 1. +// The second situation (document="abab", search string="ab") is notable for that the two occurrences +//of the search string are side-by-side with no characters in between, so some off-by-one errors +//would be detected by this test. As the first match starts at the beginning at the document the +//last match ends at the end of the document, it also detects off-by-one errors for finding the first/last match. +// The third situation (document="abababa", search string="aba") is notable for it shows whether +//the next match is allowed to contain letters from the previous one: currently it is not +//(as in the majority of browsers, viewers and editors), and therefore "abababa" is considered to +//contain not three but two occurrences of "aba" (if one starts search from the beginning of the document). +// The fourth situation (document="a ba b", search string="a b") demonstrates the case when one TinyTextEntity +//contains multiple characters that are contained in different matches (namely, the middle "ba" is one TinyTextEntity); +//in particular, since these matches are side-by-side, this test would detect some off-by-one +//offset errors. + +void SearchTest::testNextAndPrevious() +{ + +#define TEST_NEXT_PREV_SITUATION_COUNT 4 + + QVector texts[TEST_NEXT_PREV_SITUATION_COUNT] = { + QVector() << "a" << "b" << "a" << "b" << "a", + QVector() << "a" << "b" << "a" << "b", + QVector() << "a" << "b" << "a" << "b" << "a" << "b" << "a" , + QVector() << "a" << " " << "ba" << " " << "b" + }; + + QString searchStrings[TEST_NEXT_PREV_SITUATION_COUNT] = { + "b", + "ab", + "aba", + "a b" + }; + + for (int i = 0; i < TEST_NEXT_PREV_SITUATION_COUNT; i++) { + const QVector& text = texts[i]; + const QString& searchString = searchStrings[i]; + const int n = text.size(); + + Okular::NormalizedRect* rect = new Okular::NormalizedRect[n]; \ + + for (int i = 0; i < n; i++) { + rect[i] = Okular::NormalizedRect(0.1*i, 0.0, 0.1*(i+1), 0.1); \ + } + + Okular::Page* page; + Okular::TextPage* tp = createTextPage(text.data(), rect, n, page); + + //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 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); + + delete page; + delete[] rect; + } } void SearchTest::test311232() { - Okular::SettingsCore::instance( "searchtest" ); Okular::Document d(0); SearchFinishedReceiver receiver; QSignalSpy spy(&d, SIGNAL(searchFinished(int,Okular::Document::SearchStatus))); @@ -77,6 +192,197 @@ void SearchTest::test311232() QCOMPARE(receiver.m_status, Okular::Document::NoMatchFound); } +void SearchTest::test323262() +{ + QString text[] = { + "a\n" + }; + Okular::NormalizedRect rect[] = { + Okular::NormalizedRect(1, 2, 3, 4) + }; + + Okular::Page* page; + Okular::TextPage* tp = createTextPage(text, rect, 1, page); + + Okular::RegularAreaRect* result = tp->findText(0, "a", Okular::FromBottom, Qt::CaseSensitive, NULL); + QVERIFY(result); + delete result; + + delete page; +} + +void SearchTest::test323263() +{ + QString text[] = { + "a", "a", "b" + }; + Okular::NormalizedRect rect[] = { + Okular::NormalizedRect(0, 0, 1, 1), + Okular::NormalizedRect(1, 0, 2, 1), + Okular::NormalizedRect(2, 0, 3, 1) + }; + + Okular::Page* page; + Okular::TextPage* tp = createTextPage(text, rect, 3, page); + + Okular::RegularAreaRect* result = tp->findText(0, "ab", Okular::FromTop, Qt::CaseSensitive, NULL); + QVERIFY(result); + Okular::RegularAreaRect expected; + expected.append(rect[1]); + expected.append(rect[2]); + expected.simplify(); + QCOMPARE(*result, expected); + delete result; + + delete page; +} + +void SearchTest::testDottedI() +{ + //Earlier versions of okular had the bug that the letter "İ" (capital dotter i) did not match itself + //in case-insensitive mode (this was caused by an unnecessary call of toLower() and the fact that + //QString::fromUtf8("İ").compare(QString::fromUtf8("İ").toLower(), Qt::CaseInsensitive) == FALSE, + //at least in Qt 4.8). + + //In the future it would be nice to add support for matching "İ"<->"i" and "I"<->"ı" in case-insensitive + //mode as well (QString::compare does not match them, at least in non-Turkish locales, since it follows + //the Unicode case-folding rules http://www.unicode.org/Public/6.2.0/ucd/CaseFolding.txt). + + QString text[] = { + QString::fromUtf8("İ") + }; + Okular::NormalizedRect rect[] = { + Okular::NormalizedRect(1, 2, 3, 4) + }; + + Okular::Page* page; + Okular::TextPage* tp = createTextPage(text, rect, 1, page); + + Okular::RegularAreaRect* result = tp->findText(0, QString::fromUtf8("İ"), Okular::FromTop, Qt::CaseInsensitive, NULL); + QVERIFY(result); + delete result; + + delete page; +} + +void SearchTest::testHyphenAtEndOfLineWithoutYOverlap() { + QString text[] = { + "super-", + "cali-\n", + "fragilistic", "-", + "expiali", "-\n", + "docious" + }; + + Okular::NormalizedRect rect[] = { + Okular::NormalizedRect(0.4, 0.0, 0.9, 0.1), + Okular::NormalizedRect(0.0, 0.1, 0.6, 0.2), + Okular::NormalizedRect(0.0, 0.2, 0.8, 0.3), Okular::NormalizedRect(0.8, 0.2, 0.9, 0.3), + Okular::NormalizedRect(0.0, 0.3, 0.8, 0.4), Okular::NormalizedRect(0.8, 0.3, 0.9, 0.4), + Okular::NormalizedRect(0.0, 0.4, 0.7, 0.5) + }; + + unsigned n = sizeof(text)/sizeof(QString); + QCOMPARE(n, sizeof(rect)/sizeof(Okular::NormalizedRect)); + + Okular::Page* page; + Okular::TextPage* tp = createTextPage(text, rect, n, page); + + Okular::RegularAreaRect* result = tp->findText(0, "supercalifragilisticexpialidocious", + Okular::FromTop, Qt::CaseSensitive, NULL); + QVERIFY(result); + Okular::RegularAreaRect expected; + for (unsigned i = 0; i < n; i++) { + expected.append(rect[i]); + } + expected.simplify(); + QCOMPARE(*result, expected); + delete result; + + delete page; +} + +static bool testHyphen(Okular::NormalizedRect rect[], QString searchString) { + QString text[] = { + "a-", + "b" + }; + + int n = 2; + + Okular::Page* page; + Okular::TextPage* tp = createTextPage(text, rect, n, page); + + Okular::RegularAreaRect* result = tp->findText(0, searchString, + Okular::FromTop, Qt::CaseSensitive, NULL); + + bool found = result; + + delete result; + delete page; + + return found; +} + +void SearchTest::testHyphenWithYOverlap() { + Okular::NormalizedRect rect[2]; + + //different lines (50% y-coordinate overlap), first rectangle has larger height + rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.9, 0.35); + rect[1] = Okular::NormalizedRect(0.0, 0.3, 0.2, 0.4); + QVERIFY(testHyphen(rect, "ab")); + + //different lines (50% y-coordinate overlap), second rectangle has larger height + rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.9, 0.1); + rect[1] = Okular::NormalizedRect(0.0, 0.05, 0.2, 0.4); + QVERIFY(testHyphen(rect, "ab")); + + //same line (90% y-coordinate overlap), first rectangle has larger height + rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.4, 0.2); + rect[1] = Okular::NormalizedRect(0.4, 0.11, 0.6, 0.21); + QVERIFY(!testHyphen(rect, "ab")); + QVERIFY(testHyphen(rect, "a-b")); + + //same line (90% y-coordinate overlap), second rectangle has larger height + rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.4, 0.1); + rect[1] = Okular::NormalizedRect(0.4, 0.01, 0.6, 0.2); + QVERIFY(!testHyphen(rect, "ab")); + QVERIFY(testHyphen(rect, "a-b")); +} + +void SearchTest::testHyphenAtEndOfPage() { + //Tests for segmentation fault that would occur if + //we tried look ahead (for determining whether the + //next character is at the same line) at the end of the page. + + QString text[] = { + "a-" + }; + + Okular::NormalizedRect rect[] = { + Okular::NormalizedRect(0, 0, 1, 1) + }; + + Okular::Page* page; + Okular::TextPage* tp = createTextPage(text, rect, 1, page); + + { + Okular::RegularAreaRect* result = tp->findText(0, "a", + Okular::FromTop, Qt::CaseSensitive, NULL); + QVERIFY(result); + delete result; + } + + { + Okular::RegularAreaRect* result = tp->findText(0, "a", + Okular::FromBottom, Qt::CaseSensitive, NULL); + QVERIFY(result); + delete result; + } + + delete page; +} + QTEST_KDEMAIN( SearchTest, GUI ) #include "searchtest.moc" diff --git a/ui/videowidget.cpp b/ui/videowidget.cpp index 0a3db9994..55bef6b0b 100644 --- a/ui/videowidget.cpp +++ b/ui/videowidget.cpp @@ -358,6 +358,7 @@ void VideoWidget::pageLeft() void VideoWidget::play() { + d->controlBar->setVisible( d->movie->showControls() ); d->load(); d->player->play(); d->stopAction->setEnabled( true );