KDEUI
kfind.cpp
Go to the documentation of this file.
00001 /* 00002 Copyright (C) 2001, S.R.Haque <srhaque@iee.org>. 00003 Copyright (C) 2002, David Faure <david@mandrakesoft.com> 00004 Copyright (C) 2004, Arend van Beelen jr. <arend@auton.nl> 00005 This file is part of the KDE project 00006 00007 This library is free software; you can redistribute it and/or 00008 modify it under the terms of the GNU Library General Public 00009 License version 2, as published by the Free Software Foundation. 00010 00011 This library is distributed in the hope that it will be useful, 00012 but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00014 Library General Public License for more details. 00015 00016 You should have received a copy of the GNU Library General Public License 00017 along with this library; see the file COPYING.LIB. If not, write to 00018 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 00019 Boston, MA 02110-1301, USA. 00020 */ 00021 00022 #include "kfind.h" 00023 #include "kfind_p.h" 00024 #include "kfinddialog.h" 00025 00026 #include <klocale.h> 00027 #include <kmessagebox.h> 00028 #include <kdebug.h> 00029 00030 #include <QtGui/QLabel> 00031 #include <QtCore/QRegExp> 00032 #include <QtCore/QHash> 00033 #include <QTextDocument> 00034 00035 // #define DEBUG_FIND 00036 00037 static const int INDEX_NOMATCH = -1; 00038 00039 class KFindNextDialog : public KDialog 00040 { 00041 public: 00042 KFindNextDialog(const QString &pattern, QWidget *parent); 00043 }; 00044 00045 // Create the dialog. 00046 KFindNextDialog::KFindNextDialog(const QString &pattern, QWidget *parent) : 00047 KDialog(parent) 00048 { 00049 setModal( false ); 00050 setCaption( i18n("Find Next") ); 00051 setButtons( User1 | Close ); 00052 setButtonGuiItem( User1, KStandardGuiItem::find() ); 00053 setDefaultButton( User1 ); 00054 00055 setMainWidget( new QLabel( i18n("<qt>Find next occurrence of '<b>%1</b>'?</qt>", pattern), this ) ); 00056 } 00057 00059 00060 00061 KFind::KFind( const QString &pattern, long options, QWidget *parent ) 00062 : QObject( parent ), 00063 d(new KFind::Private(this)) 00064 { 00065 d->options = options; 00066 d->init( pattern ); 00067 } 00068 00069 KFind::KFind( const QString &pattern, long options, QWidget *parent, QWidget *findDialog ) 00070 : QObject( parent ), 00071 d(new KFind::Private(this)) 00072 { 00073 d->findDialog = findDialog; 00074 d->options = options; 00075 d->init( pattern ); 00076 } 00077 00078 void KFind::Private::init( const QString& _pattern ) 00079 { 00080 matches = 0; 00081 pattern = _pattern; 00082 dialog = 0; 00083 dialogClosed = false; 00084 index = INDEX_NOMATCH; 00085 lastResult = NoMatch; 00086 regExp = 0; 00087 q->setOptions( options ); // create d->regExp with the right options 00088 } 00089 00090 KFind::~KFind() 00091 { 00092 delete d; 00093 kDebug() ; 00094 } 00095 00096 bool KFind::needData() const 00097 { 00098 // always true when d->text is empty. 00099 if (d->options & KFind::FindBackwards) 00100 // d->index==-1 and d->lastResult==Match means we haven't answered nomatch yet 00101 // This is important in the "replace with a prompt" case. 00102 return ( d->index < 0 && d->lastResult != Match ); 00103 else 00104 // "index over length" test removed: we want to get a nomatch before we set data again 00105 // This is important in the "replace with a prompt" case. 00106 return d->index == INDEX_NOMATCH; 00107 } 00108 00109 void KFind::setData( const QString& data, int startPos ) 00110 { 00111 setData( -1, data, startPos ); 00112 } 00113 00114 void KFind::setData( int id, const QString& data, int startPos ) 00115 { 00116 // cache the data for incremental find 00117 if ( d->options & KFind::FindIncremental ) 00118 { 00119 if ( id != -1 ) 00120 d->customIds = true; 00121 else 00122 id = d->currentId + 1; 00123 00124 Q_ASSERT( id <= d->data.size() ); 00125 00126 if ( id == d->data.size() ) 00127 d->data.append( Private::Data(id, data, true) ); 00128 else 00129 d->data.replace( id, Private::Data(id, data, true) ); 00130 Q_ASSERT( d->data.at(id).text == data ); 00131 } 00132 00133 if ( !(d->options & KFind::FindIncremental) || needData() ) 00134 { 00135 d->text = data; 00136 00137 if ( startPos != -1 ) 00138 d->index = startPos; 00139 else if (d->options & KFind::FindBackwards) 00140 d->index = d->text.length(); 00141 else 00142 d->index = 0; 00143 #ifdef DEBUG_FIND 00144 kDebug() << "setData: '" << d->text << "' d->index=" << d->index; 00145 #endif 00146 Q_ASSERT( d->index != INDEX_NOMATCH ); 00147 d->lastResult = NoMatch; 00148 00149 d->currentId = id; 00150 } 00151 } 00152 00153 KDialog* KFind::findNextDialog( bool create ) 00154 { 00155 if ( !d->dialog && create ) 00156 { 00157 d->dialog = new KFindNextDialog( d->pattern, parentWidget() ); 00158 connect( d->dialog, SIGNAL( user1Clicked() ), this, SLOT( _k_slotFindNext() ) ); 00159 connect( d->dialog, SIGNAL( finished() ), this, SLOT( _k_slotDialogClosed() ) ); 00160 } 00161 return d->dialog; 00162 } 00163 00164 KFind::Result KFind::find() 00165 { 00166 Q_ASSERT( d->index != INDEX_NOMATCH || d->patternChanged ); 00167 00168 if ( d->lastResult == Match && !d->patternChanged ) 00169 { 00170 // Move on before looking for the next match, _if_ we just found a match 00171 if (d->options & KFind::FindBackwards) { 00172 d->index--; 00173 if ( d->index == -1 ) // don't call KFind::find with -1, it has a special meaning 00174 { 00175 d->lastResult = NoMatch; 00176 return NoMatch; 00177 } 00178 } else 00179 d->index++; 00180 } 00181 d->patternChanged = false; 00182 00183 if ( d->options & KFind::FindIncremental ) 00184 { 00185 // if the current pattern is shorter than the matchedPattern we can 00186 // probably look up the match in the incrementalPath 00187 if ( d->pattern.length() < d->matchedPattern.length() ) 00188 { 00189 Private::Match match; 00190 if ( !d->pattern.isEmpty() ) 00191 match = d->incrementalPath.value( d->pattern ); 00192 else if ( d->emptyMatch ) 00193 match = *d->emptyMatch; 00194 QString previousPattern (d->matchedPattern); 00195 d->matchedPattern = d->pattern; 00196 if ( !match.isNull() ) 00197 { 00198 bool clean = true; 00199 00200 // find the first result backwards on the path that isn't dirty 00201 while ( d->data.at(match.dataId).dirty == true && 00202 !d->pattern.isEmpty() ) 00203 { 00204 d->pattern.truncate( d->pattern.length() - 1 ); 00205 00206 match = d->incrementalPath.value( d->pattern ); 00207 00208 clean = false; 00209 } 00210 00211 // remove all matches that lie after the current match 00212 while ( d->pattern.length() < previousPattern.length() ) 00213 { 00214 d->incrementalPath.remove(previousPattern); 00215 previousPattern.truncate(previousPattern.length() - 1); 00216 } 00217 00218 // set the current text, index, etc. to the found match 00219 d->text = d->data.at(match.dataId).text; 00220 d->index = match.index; 00221 d->matchedLength = match.matchedLength; 00222 d->currentId = match.dataId; 00223 00224 // if the result is clean we can return it now 00225 if ( clean ) 00226 { 00227 if ( d->customIds ) 00228 emit highlight(d->currentId, d->index, d->matchedLength); 00229 else 00230 emit highlight(d->text, d->index, d->matchedLength); 00231 00232 d->lastResult = Match; 00233 d->matchedPattern = d->pattern; 00234 return Match; 00235 } 00236 } 00237 // if we couldn't look up the match, the new pattern isn't a 00238 // substring of the matchedPattern, so we start a new search 00239 else 00240 { 00241 d->startNewIncrementalSearch(); 00242 } 00243 } 00244 // if the new pattern is longer than the matchedPattern we might be 00245 // able to proceed from the last search 00246 else if ( d->pattern.length() > d->matchedPattern.length() ) 00247 { 00248 // continue from the previous pattern 00249 if ( d->pattern.startsWith(d->matchedPattern) ) 00250 { 00251 // we can't proceed from the previous position if the previous 00252 // position already failed 00253 if ( d->index == INDEX_NOMATCH ) 00254 return NoMatch; 00255 00256 QString temp (d->pattern); 00257 d->pattern.truncate(d->matchedPattern.length() + 1); 00258 d->matchedPattern = temp; 00259 } 00260 // start a new search 00261 else 00262 { 00263 d->startNewIncrementalSearch(); 00264 } 00265 } 00266 // if the new pattern is as long as the matchedPattern, we reset if 00267 // they are not equal 00268 else if ( d->pattern != d->matchedPattern ) 00269 { 00270 d->startNewIncrementalSearch(); 00271 } 00272 } 00273 00274 #ifdef DEBUG_FIND 00275 kDebug() << "d->index=" << d->index; 00276 #endif 00277 do 00278 { 00279 // if we have multiple data blocks in our cache, walk through these 00280 // blocks till we either searched all blocks or we find a match 00281 do 00282 { 00283 // Find the next candidate match. 00284 if ( d->options & KFind::RegularExpression ) 00285 d->index = KFind::find(d->text, *d->regExp, d->index, d->options, &d->matchedLength); 00286 else 00287 d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength); 00288 00289 if ( d->options & KFind::FindIncremental ) 00290 d->data[d->currentId].dirty = false; 00291 00292 if (d->index == -1 && d->currentId < d->data.count() - 1) { 00293 d->text = d->data.at(++d->currentId).text; 00294 00295 if ( d->options & KFind::FindBackwards ) 00296 d->index = d->text.length(); 00297 else 00298 d->index = 0; 00299 } else { 00300 break; 00301 } 00302 } while ( !(d->options & KFind::RegularExpression) ); 00303 00304 if ( d->index != -1 ) 00305 { 00306 // Flexibility: the app can add more rules to validate a possible match 00307 if ( validateMatch( d->text, d->index, d->matchedLength ) ) 00308 { 00309 bool done = true; 00310 00311 if ( d->options & KFind::FindIncremental ) 00312 { 00313 if ( d->pattern.isEmpty() ) { 00314 delete d->emptyMatch; 00315 d->emptyMatch = new Private::Match( d->currentId, d->index, d->matchedLength ); 00316 } else 00317 d->incrementalPath.insert(d->pattern, Private::Match(d->currentId, d->index, d->matchedLength)); 00318 00319 if ( d->pattern.length() < d->matchedPattern.length() ) 00320 { 00321 d->pattern += d->matchedPattern.mid(d->pattern.length(), 1); 00322 done = false; 00323 } 00324 } 00325 00326 if ( done ) 00327 { 00328 d->matches++; 00329 // Tell the world about the match we found, in case someone wants to 00330 // highlight it. 00331 if ( d->customIds ) 00332 emit highlight(d->currentId, d->index, d->matchedLength); 00333 else 00334 emit highlight(d->text, d->index, d->matchedLength); 00335 00336 if ( !d->dialogClosed ) 00337 findNextDialog(true)->show(); 00338 00339 #ifdef DEBUG_FIND 00340 kDebug() << "Match. Next d->index=" << d->index; 00341 #endif 00342 d->lastResult = Match; 00343 return Match; 00344 } 00345 } 00346 else // Skip match 00347 { 00348 if (d->options & KFind::FindBackwards) 00349 d->index--; 00350 else 00351 d->index++; 00352 } 00353 } 00354 else 00355 { 00356 if ( d->options & KFind::FindIncremental ) 00357 { 00358 QString temp (d->pattern); 00359 temp.truncate(temp.length() - 1); 00360 d->pattern = d->matchedPattern; 00361 d->matchedPattern = temp; 00362 } 00363 00364 d->index = INDEX_NOMATCH; 00365 } 00366 } 00367 while (d->index != INDEX_NOMATCH); 00368 00369 #ifdef DEBUG_FIND 00370 kDebug() << "NoMatch. d->index=" << d->index; 00371 #endif 00372 d->lastResult = NoMatch; 00373 return NoMatch; 00374 } 00375 00376 void KFind::Private::startNewIncrementalSearch() 00377 { 00378 Private::Match *match = emptyMatch; 00379 if(match == 0) 00380 { 00381 text.clear(); 00382 index = 0; 00383 currentId = 0; 00384 } 00385 else 00386 { 00387 text = data.at(match->dataId).text; 00388 index = match->index; 00389 currentId = match->dataId; 00390 } 00391 matchedLength = 0; 00392 incrementalPath.clear(); 00393 delete emptyMatch; emptyMatch = 0; 00394 matchedPattern = pattern; 00395 pattern.clear(); 00396 } 00397 00398 static bool isInWord(QChar ch) 00399 { 00400 return ch.isLetter() || ch.isDigit() || ch == '_'; 00401 } 00402 00403 static bool isWholeWords(const QString &text, int starts, int matchedLength) 00404 { 00405 if (starts == 0 || !isInWord(text.at(starts-1))) 00406 { 00407 const int ends = starts + matchedLength; 00408 if (ends == text.length() || !isInWord(text.at(ends))) { 00409 return true; 00410 } 00411 } 00412 return false; 00413 } 00414 00415 static bool matchOk(const QString& text, int index, int matchedLength, long options) 00416 { 00417 if (options & KFind::WholeWordsOnly) { 00418 // Is the match delimited correctly? 00419 if (isWholeWords(text, index, matchedLength)) 00420 return true; 00421 } else { 00422 // Non-whole-word search: this match is good 00423 return true; 00424 } 00425 return false; 00426 } 00427 00428 // static 00429 int KFind::find(const QString &text, const QString &pattern, int index, long options, int *matchedLength) 00430 { 00431 // Handle regular expressions in the appropriate way. 00432 if (options & KFind::RegularExpression) 00433 { 00434 Qt::CaseSensitivity caseSensitive = (options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive; 00435 QRegExp regExp(pattern, caseSensitive); 00436 00437 return find(text, regExp, index, options, matchedLength); 00438 } 00439 00440 // In Qt4 QString("aaaaaa").lastIndexOf("a",6) returns -1; we need 00441 // to start at text.length() - pattern.length() to give a valid index to QString. 00442 if (options & KFind::FindBackwards) { 00443 index = qMin( qMax(0, text.length() - pattern.length()), index ); 00444 } 00445 00446 Qt::CaseSensitivity caseSensitive = (options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive; 00447 00448 if (options & KFind::FindBackwards) { 00449 // Backward search, until the beginning of the line... 00450 while (index >= 0) { 00451 // ...find the next match. 00452 index = text.lastIndexOf(pattern, index, caseSensitive); 00453 if (index == -1) 00454 break; 00455 00456 if (matchOk(text, index, pattern.length(), options)) 00457 break; 00458 index--; 00459 kDebug() << "decrementing:" << index; 00460 } 00461 } else { 00462 // Forward search, until the end of the line... 00463 while (index <= text.length()) 00464 { 00465 // ...find the next match. 00466 index = text.indexOf(pattern, index, caseSensitive); 00467 if (index == -1) 00468 break; 00469 00470 if (matchOk(text, index, pattern.length(), options)) 00471 break; 00472 index++; 00473 } 00474 if (index > text.length()) { // end of line 00475 kDebug() << "at" << index << "-> not found"; 00476 index = -1; // not found 00477 } 00478 } 00479 if (index <= -1) 00480 *matchedLength = 0; 00481 else 00482 *matchedLength = pattern.length(); 00483 return index; 00484 } 00485 00486 // Core method for the regexp-based find 00487 static int doFind(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength) 00488 { 00489 if (options & KFind::FindBackwards) { 00490 // Backward search, until the beginning of the line... 00491 while (index >= 0) { 00492 // ...find the next match. 00493 index = text.lastIndexOf(pattern, index); 00494 if (index == -1) 00495 break; 00496 00497 /*int pos =*/ pattern.indexIn( text.mid(index) ); 00498 *matchedLength = pattern.matchedLength(); 00499 if (matchOk(text, index, *matchedLength, options)) 00500 break; 00501 index--; 00502 } 00503 } else { 00504 // Forward search, until the end of the line... 00505 while (index <= text.length()) { 00506 // ...find the next match. 00507 index = text.indexOf(pattern, index); 00508 if (index == -1) 00509 break; 00510 00511 /*int pos =*/ pattern.indexIn( text.mid(index) ); 00512 *matchedLength = pattern.matchedLength(); 00513 if (matchOk(text, index, *matchedLength, options)) 00514 break; 00515 index++; 00516 } 00517 if (index > text.length()) { // end of line 00518 index = -1; // not found 00519 } 00520 } 00521 if (index == -1) 00522 *matchedLength = 0; 00523 return index; 00524 } 00525 00526 // Since QRegExp doesn't support multiline searches (the equivalent of perl's /m) 00527 // we have to cut the text into lines if the pattern starts with ^ or ends with $. 00528 static int lineBasedFind(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength) 00529 { 00530 const QStringList lines = text.split('\n'); 00531 int offset = 0; 00532 // Use "index" to find the first line we should start from 00533 int startLineNumber = 0; 00534 for (; startLineNumber < lines.count(); ++startLineNumber) { 00535 const QString line = lines.at(startLineNumber); 00536 if (index < offset + line.length()) { 00537 break; 00538 } 00539 offset += line.length() + 1 /*newline*/; 00540 } 00541 00542 if (options & KFind::FindBackwards) { 00543 00544 if (startLineNumber == lines.count()) { 00545 // We went too far, go back to the last line 00546 --startLineNumber; 00547 offset -= lines.at(startLineNumber).length() + 1; 00548 } 00549 00550 for (int lineNumber = startLineNumber; lineNumber >= 0; --lineNumber) { 00551 const QString line = lines.at(lineNumber); 00552 const int ret = doFind(line, pattern, lineNumber == startLineNumber ? index - offset : line.length(), options, matchedLength); 00553 if (ret > -1) 00554 return ret + offset; 00555 offset -= line.length() + 1 /*newline*/; 00556 } 00557 00558 } else { 00559 for (int lineNumber = startLineNumber; lineNumber < lines.count(); ++lineNumber) { 00560 const QString line = lines.at(lineNumber); 00561 const int ret = doFind(line, pattern, lineNumber == startLineNumber ? (index - offset) : 0, options, matchedLength); 00562 if (ret > -1) { 00563 return ret + offset; 00564 } 00565 offset += line.length() + 1 /*newline*/; 00566 } 00567 } 00568 return -1; 00569 } 00570 00571 // static 00572 int KFind::find(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength) 00573 { 00574 if (pattern.pattern().startsWith('^') || pattern.pattern().endsWith('$')) { 00575 return lineBasedFind(text, pattern, index, options, matchedLength); 00576 } 00577 00578 return doFind(text, pattern, index, options, matchedLength); 00579 } 00580 00581 void KFind::Private::_k_slotFindNext() 00582 { 00583 emit q->findNext(); 00584 } 00585 00586 void KFind::Private::_k_slotDialogClosed() 00587 { 00588 #ifdef DEBUG_FIND 00589 kDebug() << " Begin"; 00590 #endif 00591 emit q->dialogClosed(); 00592 dialogClosed = true; 00593 #ifdef DEBUG_FIND 00594 kDebug() << " End"; 00595 #endif 00596 00597 } 00598 00599 void KFind::displayFinalDialog() const 00600 { 00601 QString message; 00602 if ( numMatches() ) 00603 message = i18np( "1 match found.", "%1 matches found.", numMatches() ); 00604 else 00605 message = i18n("<qt>No matches found for '<b>%1</b>'.</qt>", Qt::escape(d->pattern)); 00606 KMessageBox::information(dialogsParent(), message); 00607 } 00608 00609 bool KFind::shouldRestart( bool forceAsking, bool showNumMatches ) const 00610 { 00611 // Only ask if we did a "find from cursor", otherwise it's pointless. 00612 // Well, unless the user can modify the document during a search operation, 00613 // hence the force boolean. 00614 if ( !forceAsking && (d->options & KFind::FromCursor) == 0 ) 00615 { 00616 displayFinalDialog(); 00617 return false; 00618 } 00619 QString message; 00620 if ( showNumMatches ) 00621 { 00622 if ( numMatches() ) 00623 message = i18np( "1 match found.", "%1 matches found.", numMatches() ); 00624 else 00625 message = i18n("No matches found for '<b>%1</b>'.", Qt::escape(d->pattern)); 00626 } 00627 else 00628 { 00629 if ( d->options & KFind::FindBackwards ) 00630 message = i18n( "Beginning of document reached." ); 00631 else 00632 message = i18n( "End of document reached." ); 00633 } 00634 00635 message += "<br><br>"; // can't be in the i18n() of the first if() because of the plural form. 00636 // Hope this word puzzle is ok, it's a different sentence 00637 message += 00638 ( d->options & KFind::FindBackwards ) ? 00639 i18n("Continue from the end?") 00640 : i18n("Continue from the beginning?"); 00641 00642 int ret = KMessageBox::questionYesNo( dialogsParent(), "<qt>"+message+"</qt>", 00643 QString(), KStandardGuiItem::cont(), KStandardGuiItem::stop() ); 00644 bool yes = ( ret == KMessageBox::Yes ); 00645 if ( yes ) 00646 const_cast<KFind*>(this)->d->options &= ~KFind::FromCursor; // clear FromCursor option 00647 return yes; 00648 } 00649 00650 long KFind::options() const 00651 { 00652 return d->options; 00653 } 00654 00655 void KFind::setOptions( long options ) 00656 { 00657 d->options = options; 00658 00659 delete d->regExp; 00660 if (d->options & KFind::RegularExpression) { 00661 Qt::CaseSensitivity caseSensitive = (d->options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive; 00662 d->regExp = new QRegExp(d->pattern, caseSensitive); 00663 } else 00664 d->regExp = 0; 00665 } 00666 00667 void KFind::closeFindNextDialog() 00668 { 00669 if (d->dialog) { 00670 d->dialog->deleteLater(); 00671 d->dialog = 0; 00672 } 00673 d->dialogClosed = true; 00674 } 00675 00676 int KFind::index() const 00677 { 00678 return d->index; 00679 } 00680 00681 QString KFind::pattern() const 00682 { 00683 return d->pattern; 00684 } 00685 00686 void KFind::setPattern( const QString& pattern ) 00687 { 00688 if ( d->options & KFind::FindIncremental && d->pattern != pattern ) 00689 d->patternChanged = true; 00690 00691 d->pattern = pattern; 00692 setOptions( options() ); // rebuild d->regExp if necessary 00693 } 00694 00695 int KFind::numMatches() const 00696 { 00697 return d->matches; 00698 } 00699 00700 void KFind::resetCounts() 00701 { 00702 d->matches = 0; 00703 } 00704 00705 bool KFind::validateMatch( const QString &, int, int ) 00706 { 00707 return true; 00708 } 00709 00710 QWidget* KFind::parentWidget() const 00711 { 00712 return (QWidget *)parent(); 00713 } 00714 00715 QWidget* KFind::dialogsParent() const 00716 { 00717 // If the find dialog is still up, it should get the focus when closing a message box 00718 // Otherwise, maybe the "find next?" dialog is up 00719 // Otherwise, the "view" is the parent. 00720 return d->findDialog ? (QWidget*)d->findDialog : ( d->dialog ? d->dialog : parentWidget() ); 00721 } 00722 00723 #include "kfind.moc"
KDE 4.6 API Reference