kutils Library API Documentation

kfind.cpp

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., 59 Temple Place - Suite 330, 00019 Boston, MA 02111-1307, USA. 00020 */ 00021 00022 #include "kfind.h" 00023 #include "kfinddialog.h" 00024 #include <kapplication.h> 00025 #include <klocale.h> 00026 #include <kmessagebox.h> 00027 #include <qlabel.h> 00028 #include <qregexp.h> 00029 #include <qstylesheet.h> 00030 #include <qguardedptr.h> 00031 #include <qptrvector.h> 00032 #include <kdebug.h> 00033 00034 //#define DEBUG_FIND 00035 00036 #define INDEX_NOMATCH -1 00037 00038 class KFindNextDialog : public KDialogBase 00039 { 00040 public: 00041 KFindNextDialog(const QString &pattern, QWidget *parent); 00042 }; 00043 00044 // Create the dialog. 00045 KFindNextDialog::KFindNextDialog(const QString &pattern, QWidget *parent) : 00046 KDialogBase(parent, 0, false, // non-modal! 00047 i18n("Find Next"), 00048 User1 | Close, 00049 User1, 00050 false, 00051 i18n("&Find")) 00052 { 00053 setMainWidget( new QLabel( i18n("<qt>Find next occurrence of '<b>%1</b>'?</qt>").arg(pattern), this ) ); 00054 } 00055 00057 00058 struct KFind::Private 00059 { 00060 Private() : 00061 findDialog(0), 00062 patternChanged(false), 00063 matchedPattern(""), 00064 incrementalPath(29, true), 00065 currentId(0), 00066 customIds(false) 00067 { 00068 incrementalPath.setAutoDelete(true); 00069 data.setAutoDelete(true); 00070 } 00071 00072 struct Match 00073 { 00074 Match(int dataId, int index, int matchedLength) : 00075 dataId(dataId), 00076 index(index), 00077 matchedLength(matchedLength) 00078 { } 00079 00080 int dataId; 00081 int index; 00082 int matchedLength; 00083 }; 00084 00085 struct Data 00086 { 00087 Data() : id(-1), dirty(false) { } 00088 Data(int id, const QString &text, bool dirty = false) : 00089 id(id), 00090 text(text), 00091 dirty(dirty) 00092 { } 00093 00094 int id; 00095 QString text; 00096 bool dirty; 00097 }; 00098 00099 QGuardedPtr<QWidget> findDialog; 00100 bool patternChanged; 00101 QString matchedPattern; 00102 QDict<Match> incrementalPath; 00103 QPtrVector<Data> data; 00104 int currentId; 00105 bool customIds; 00106 }; 00107 00109 00110 KFind::KFind( const QString &pattern, long options, QWidget *parent ) 00111 : QObject( parent ) 00112 { 00113 d = new KFind::Private; 00114 m_options = options; 00115 init( pattern ); 00116 } 00117 00118 KFind::KFind( const QString &pattern, long options, QWidget *parent, QWidget *findDialog ) 00119 : QObject( parent ) 00120 { 00121 d = new KFind::Private; 00122 d->findDialog = findDialog; 00123 m_options = options; 00124 init( pattern ); 00125 } 00126 00127 void KFind::init( const QString& pattern ) 00128 { 00129 m_matches = 0; 00130 m_pattern = pattern; 00131 m_dialog = 0; 00132 m_dialogClosed = false; 00133 m_index = INDEX_NOMATCH; 00134 m_lastResult = NoMatch; 00135 if (m_options & KFindDialog::RegularExpression) 00136 m_regExp = new QRegExp(pattern, m_options & KFindDialog::CaseSensitive); 00137 else { 00138 m_regExp = 0; 00139 } 00140 } 00141 00142 KFind::~KFind() 00143 { 00144 delete m_dialog; 00145 delete d; 00146 } 00147 00148 bool KFind::needData() const 00149 { 00150 // always true when m_text is empty. 00151 if (m_options & KFindDialog::FindBackwards) 00152 // m_index==-1 and m_lastResult==Match means we haven't answered nomatch yet 00153 // This is important in the "replace with a prompt" case. 00154 return ( m_index < 0 && m_lastResult != Match ); 00155 else 00156 // "index over length" test removed: we want to get a nomatch before we set data again 00157 // This is important in the "replace with a prompt" case. 00158 return m_index == INDEX_NOMATCH; 00159 } 00160 00161 void KFind::setData( const QString& data, int startPos ) 00162 { 00163 setData( -1, data, startPos ); 00164 } 00165 00166 void KFind::setData( int id, const QString& data, int startPos ) 00167 { 00168 // cache the data for incremental find 00169 if ( m_options & KFindDialog::FindIncremental ) 00170 { 00171 if ( id == -1 ) 00172 id = d->currentId + 1; 00173 00174 if ( id >= (int) d->data.size() ) 00175 d->data.resize( id + 100 ); 00176 00177 if ( id != -1 ) 00178 d->customIds = true; 00179 00180 d->data.insert( id, new Private::Data(id, data, true) ); 00181 } 00182 00183 if ( !(m_options & KFindDialog::FindIncremental) || needData() ) 00184 { 00185 m_text = data; 00186 00187 if ( startPos != -1 ) 00188 m_index = startPos; 00189 else if (m_options & KFindDialog::FindBackwards) 00190 m_index = QMAX( (int)m_text.length() - 1, 0 ); 00191 else 00192 m_index = 0; 00193 #ifdef DEBUG_FIND 00194 kdDebug() << "setData: '" << m_text << "' m_index=" << m_index << endl; 00195 #endif 00196 Q_ASSERT( m_index != INDEX_NOMATCH ); 00197 m_lastResult = NoMatch; 00198 00199 d->currentId = id; 00200 } 00201 } 00202 00203 KDialogBase* KFind::findNextDialog( bool create ) 00204 { 00205 if ( !m_dialog && create ) 00206 { 00207 m_dialog = new KFindNextDialog( m_pattern, parentWidget() ); 00208 connect( m_dialog, SIGNAL( user1Clicked() ), this, SLOT( slotFindNext() ) ); 00209 connect( m_dialog, SIGNAL( finished() ), this, SLOT( slotDialogClosed() ) ); 00210 } 00211 return m_dialog; 00212 } 00213 00214 KFind::Result KFind::find() 00215 { 00216 Q_ASSERT( m_index != INDEX_NOMATCH || d->patternChanged ); 00217 00218 if ( m_lastResult == Match && !d->patternChanged ) 00219 { 00220 // Move on before looking for the next match, _if_ we just found a match 00221 if (m_options & KFindDialog::FindBackwards) { 00222 m_index--; 00223 if ( m_index == -1 ) // don't call KFind::find with -1, it has a special meaning 00224 { 00225 m_lastResult = NoMatch; 00226 return NoMatch; 00227 } 00228 } else 00229 m_index++; 00230 } 00231 d->patternChanged = false; 00232 00233 if ( m_options & KFindDialog::FindIncremental ) 00234 { 00235 // if the current pattern is shorter than the matchedPattern we can 00236 // probably look up the match in the incrementalPath 00237 if ( m_pattern.length() < d->matchedPattern.length() ) 00238 { 00239 Private::Match *match = d->incrementalPath[m_pattern]; 00240 QString previousPattern = d->matchedPattern; 00241 d->matchedPattern = m_pattern; 00242 if ( match != 0 ) 00243 { 00244 bool clean = true; 00245 00246 // find the first result backwards on the path that isn't dirty 00247 while ( d->data[match->dataId]->dirty == true && 00248 !m_pattern.isEmpty() ) 00249 { 00250 m_pattern.truncate( m_pattern.length() - 1 ); 00251 00252 match = d->incrementalPath[m_pattern]; 00253 00254 clean = false; 00255 } 00256 00257 // remove all matches that lie after the current match 00258 while ( m_pattern.length() < previousPattern.length() ) 00259 { 00260 d->incrementalPath.remove(previousPattern); 00261 previousPattern.truncate(previousPattern.length() - 1); 00262 } 00263 00264 // set the current text, index, etc. to the found match 00265 m_text = d->data[match->dataId]->text; 00266 m_index = match->index; 00267 m_matchedLength = match->matchedLength; 00268 d->currentId = match->dataId; 00269 00270 // if the result is clean we can return it now 00271 if ( clean ) 00272 { 00273 if ( d->customIds ) 00274 emit highlight(d->currentId, m_index, m_matchedLength); 00275 else 00276 emit highlight(m_text, m_index, m_matchedLength); 00277 00278 m_lastResult = Match; 00279 d->matchedPattern = m_pattern; 00280 return Match; 00281 } 00282 } 00283 // if we couldn't look up the match, the new pattern isn't a 00284 // substring of the matchedPattern, so we start a new search 00285 else 00286 { 00287 startNewIncrementalSearch(); 00288 } 00289 } 00290 // if the new pattern is longer than the matchedPattern we might be 00291 // able to proceed from the last search 00292 else if ( m_pattern.length() > d->matchedPattern.length() ) 00293 { 00294 // continue from the previous pattern 00295 if ( m_pattern.startsWith(d->matchedPattern) ) 00296 { 00297 // we can't proceed from the previous position if the previous 00298 // position already failed 00299 if ( m_index == INDEX_NOMATCH ) 00300 return NoMatch; 00301 00302 QString temp = m_pattern; 00303 m_pattern.truncate(d->matchedPattern.length() + 1); 00304 d->matchedPattern = temp; 00305 } 00306 // start a new search 00307 else 00308 { 00309 startNewIncrementalSearch(); 00310 } 00311 } 00312 // if the new pattern is as long as the matchedPattern, we reset if 00313 // they are not equal 00314 else if ( m_pattern != d->matchedPattern ) 00315 { 00316 startNewIncrementalSearch(); 00317 } 00318 } 00319 00320 #ifdef DEBUG_FIND 00321 kdDebug() << k_funcinfo << "m_index=" << m_index << endl; 00322 #endif 00323 do 00324 { 00325 // if we have multiple data blocks in our cache, walk through these 00326 // blocks till we either searched all blocks or we find a match 00327 do 00328 { 00329 // Find the next candidate match. 00330 if ( m_options & KFindDialog::RegularExpression ) 00331 m_index = KFind::find(m_text, *m_regExp, m_index, m_options, &m_matchedLength); 00332 else 00333 m_index = KFind::find(m_text, m_pattern, m_index, m_options, &m_matchedLength); 00334 00335 if ( m_options & KFindDialog::FindIncremental ) 00336 d->data[d->currentId]->dirty = false; 00337 00338 if ( m_index == -1 && d->currentId < (int) d->data.count() - 1 ) 00339 { 00340 m_text = d->data[++d->currentId]->text; 00341 00342 if ( m_options & KFindDialog::FindBackwards ) 00343 m_index = QMAX((int) m_text.length() - 1, 0); 00344 else 00345 m_index = 0; 00346 } 00347 else 00348 break; 00349 } while ( !(m_options & KFindDialog::RegularExpression) ); 00350 00351 if ( m_index != -1 ) 00352 { 00353 // Flexibility: the app can add more rules to validate a possible match 00354 if ( validateMatch( m_text, m_index, m_matchedLength ) ) 00355 { 00356 bool done = true; 00357 00358 if ( m_options & KFindDialog::FindIncremental ) 00359 { 00360 d->incrementalPath.replace(m_pattern, new Private::Match(d->currentId, m_index, m_matchedLength)); 00361 00362 if ( m_pattern.length() < d->matchedPattern.length() ) 00363 { 00364 m_pattern += d->matchedPattern.mid(m_pattern.length(), 1); 00365 done = false; 00366 } 00367 } 00368 00369 if ( done ) 00370 { 00371 m_matches++; 00372 // Tell the world about the match we found, in case someone wants to 00373 // highlight it. 00374 if ( d->customIds ) 00375 emit highlight(d->currentId, m_index, m_matchedLength); 00376 else 00377 emit highlight(m_text, m_index, m_matchedLength); 00378 00379 if ( !m_dialogClosed ) 00380 findNextDialog(true)->show(); 00381 00382 #ifdef DEBUG_FIND 00383 kdDebug() << k_funcinfo << "Match. Next m_index=" << m_index << endl; 00384 #endif 00385 m_lastResult = Match; 00386 return Match; 00387 } 00388 } 00389 else // Skip match 00390 { 00391 if (m_options & KFindDialog::FindBackwards) 00392 m_index--; 00393 else 00394 m_index++; 00395 } 00396 } 00397 else 00398 { 00399 if ( m_options & KFindDialog::FindIncremental ) 00400 { 00401 QString temp = m_pattern; 00402 temp.truncate(temp.length() - 1); 00403 m_pattern = d->matchedPattern; 00404 d->matchedPattern = temp; 00405 } 00406 00407 m_index = INDEX_NOMATCH; 00408 } 00409 } 00410 while (m_index != INDEX_NOMATCH); 00411 00412 #ifdef DEBUG_FIND 00413 kdDebug() << k_funcinfo << "NoMatch. m_index=" << m_index << endl; 00414 #endif 00415 m_lastResult = NoMatch; 00416 return NoMatch; 00417 } 00418 00419 void KFind::startNewIncrementalSearch() 00420 { 00421 Private::Match *match = d->incrementalPath[""]; 00422 if(match == 0) 00423 { 00424 m_text = QString::null; 00425 m_index = 0; 00426 d->currentId = 0; 00427 } 00428 else 00429 { 00430 m_text = d->data[match->dataId]->text; 00431 m_index = match->index; 00432 d->currentId = match->dataId; 00433 } 00434 m_matchedLength = 0; 00435 d->incrementalPath.clear(); 00436 d->matchedPattern = m_pattern; 00437 m_pattern = QString::null; 00438 } 00439 00440 // static 00441 int KFind::find(const QString &text, const QString &pattern, int index, long options, int *matchedLength) 00442 { 00443 // Handle regular expressions in the appropriate way. 00444 if (options & KFindDialog::RegularExpression) 00445 { 00446 QRegExp regExp(pattern, options & KFindDialog::CaseSensitive); 00447 00448 return find(text, regExp, index, options, matchedLength); 00449 } 00450 00451 bool caseSensitive = (options & KFindDialog::CaseSensitive); 00452 00453 if (options & KFindDialog::WholeWordsOnly) 00454 { 00455 if (options & KFindDialog::FindBackwards) 00456 { 00457 // Backward search, until the beginning of the line... 00458 while (index >= 0) 00459 { 00460 // ...find the next match. 00461 index = text.findRev(pattern, index, caseSensitive); 00462 if (index == -1) 00463 break; 00464 00465 // Is the match delimited correctly? 00466 *matchedLength = pattern.length(); 00467 if (isWholeWords(text, index, *matchedLength)) 00468 break; 00469 index--; 00470 } 00471 } 00472 else 00473 { 00474 // Forward search, until the end of the line... 00475 while (index < (int)text.length()) 00476 { 00477 // ...find the next match. 00478 index = text.find(pattern, index, caseSensitive); 00479 if (index == -1) 00480 break; 00481 00482 // Is the match delimited correctly? 00483 *matchedLength = pattern.length(); 00484 if (isWholeWords(text, index, *matchedLength)) 00485 break; 00486 index++; 00487 } 00488 if (index >= (int)text.length()) // end of line 00489 index = -1; // not found 00490 } 00491 } 00492 else 00493 { 00494 // Non-whole-word search. 00495 if (options & KFindDialog::FindBackwards) 00496 { 00497 index = text.findRev(pattern, index, caseSensitive); 00498 } 00499 else 00500 { 00501 index = text.find(pattern, index, caseSensitive); 00502 } 00503 if (index != -1) 00504 { 00505 *matchedLength = pattern.length(); 00506 } 00507 } 00508 return index; 00509 } 00510 00511 // static 00512 int KFind::find(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength) 00513 { 00514 if (options & KFindDialog::WholeWordsOnly) 00515 { 00516 if (options & KFindDialog::FindBackwards) 00517 { 00518 // Backward search, until the beginning of the line... 00519 while (index >= 0) 00520 { 00521 // ...find the next match. 00522 index = text.findRev(pattern, index); 00523 if (index == -1) 00524 break; 00525 00526 // Is the match delimited correctly? 00527 //pattern.match(text, index, matchedLength, false); 00528 /*int pos =*/ pattern.search( text.mid(index) ); 00529 *matchedLength = pattern.matchedLength(); 00530 if (isWholeWords(text, index, *matchedLength)) 00531 break; 00532 index--; 00533 } 00534 } 00535 else 00536 { 00537 // Forward search, until the end of the line... 00538 while (index < (int)text.length()) 00539 { 00540 // ...find the next match. 00541 index = text.find(pattern, index); 00542 if (index == -1) 00543 break; 00544 00545 // Is the match delimited correctly? 00546 //pattern.match(text, index, matchedLength, false); 00547 /*int pos =*/ pattern.search( text.mid(index) ); 00548 *matchedLength = pattern.matchedLength(); 00549 if (isWholeWords(text, index, *matchedLength)) 00550 break; 00551 index++; 00552 } 00553 if (index >= (int)text.length()) // end of line 00554 index = -1; // not found 00555 } 00556 } 00557 else 00558 { 00559 // Non-whole-word search. 00560 if (options & KFindDialog::FindBackwards) 00561 { 00562 index = text.findRev(pattern, index); 00563 } 00564 else 00565 { 00566 index = text.find(pattern, index); 00567 } 00568 if (index != -1) 00569 { 00570 //pattern.match(text, index, matchedLength, false); 00571 /*int pos =*/ pattern.search( text.mid(index) ); 00572 *matchedLength = pattern.matchedLength(); 00573 } 00574 } 00575 return index; 00576 } 00577 00578 bool KFind::isInWord(QChar ch) 00579 { 00580 return ch.isLetter() || ch.isDigit() || ch == '_'; 00581 } 00582 00583 bool KFind::isWholeWords(const QString &text, int starts, int matchedLength) 00584 { 00585 if ((starts == 0) || (!isInWord(text[starts - 1]))) 00586 { 00587 int ends = starts + matchedLength; 00588 00589 if ((ends == (int)text.length()) || (!isInWord(text[ends]))) 00590 return true; 00591 } 00592 return false; 00593 } 00594 00595 void KFind::slotFindNext() 00596 { 00597 emit findNext(); 00598 } 00599 00600 void KFind::slotDialogClosed() 00601 { 00602 emit dialogClosed(); 00603 m_dialogClosed = true; 00604 } 00605 00606 void KFind::displayFinalDialog() const 00607 { 00608 QString message; 00609 if ( numMatches() ) 00610 message = i18n( "1 match found.", "%n matches found.", numMatches() ); 00611 else 00612 message = i18n("<qt>No matches found for '<b>%1</b>'.</qt>").arg(QStyleSheet::escape(m_pattern)); 00613 KMessageBox::information(dialogsParent(), message); 00614 } 00615 00616 bool KFind::shouldRestart( bool forceAsking, bool showNumMatches ) const 00617 { 00618 // Only ask if we did a "find from cursor", otherwise it's pointless. 00619 // Well, unless the user can modify the document during a search operation, 00620 // hence the force boolean. 00621 if ( !forceAsking && (m_options & KFindDialog::FromCursor) == 0 ) 00622 { 00623 displayFinalDialog(); 00624 return false; 00625 } 00626 QString message; 00627 if ( showNumMatches ) 00628 { 00629 if ( numMatches() ) 00630 message = i18n( "1 match found.", "%n matches found.", numMatches() ); 00631 else 00632 message = i18n("No matches found for '<b>%1</b>'.").arg(QStyleSheet::escape(m_pattern)); 00633 } 00634 else 00635 { 00636 if ( m_options & KFindDialog::FindBackwards ) 00637 message = i18n( "Beginning of document reached." ); 00638 else 00639 message = i18n( "End of document reached." ); 00640 } 00641 00642 message += "\n"; // can't be in the i18n() of the first if() because of the plural form. 00643 // Hope this word puzzle is ok, it's a different sentence 00644 message += 00645 ( m_options & KFindDialog::FindBackwards ) ? 00646 i18n("Do you want to restart search from the end?") 00647 : i18n("Do you want to restart search at the beginning?"); 00648 00649 int ret = KMessageBox::questionYesNo( dialogsParent(), QString("<qt>")+message+QString("</qt>") ); 00650 bool yes = ( ret == KMessageBox::Yes ); 00651 if ( yes ) 00652 const_cast<KFind*>(this)->m_options &= ~KFindDialog::FromCursor; // clear FromCursor option 00653 return yes; 00654 } 00655 00656 void KFind::setOptions( long options ) 00657 { 00658 m_options = options; 00659 00660 delete m_regExp; 00661 if (m_options & KFindDialog::RegularExpression) 00662 m_regExp = new QRegExp(m_pattern, m_options & KFindDialog::CaseSensitive); 00663 else 00664 m_regExp = 0; 00665 } 00666 00667 void KFind::closeFindNextDialog() 00668 { 00669 delete m_dialog; 00670 m_dialog = 0L; 00671 m_dialogClosed = true; 00672 } 00673 00674 int KFind::index() const 00675 { 00676 return m_index; 00677 } 00678 00679 void KFind::setPattern( const QString& pattern ) 00680 { 00681 if ( m_options & KFindDialog::FindIncremental && m_pattern != pattern ) 00682 d->patternChanged = true; 00683 00684 m_pattern = pattern; 00685 setOptions( options() ); // rebuild m_regExp if necessary 00686 } 00687 00688 QWidget* KFind::dialogsParent() const 00689 { 00690 // If the find dialog is still up, it should get the focus when closing a message box 00691 // Otherwise, maybe the "find next?" dialog is up 00692 // Otherwise, the "view" is the parent. 00693 return d->findDialog ? (QWidget*)d->findDialog : ( m_dialog ? m_dialog : parentWidget() ); 00694 } 00695 00696 #include "kfind.moc"
KDE Logo
This file is part of the documentation for kutils Library Version 3.3.1.
Documentation copyright © 1996-2004 the KDE developers.
Generated on Sun Oct 17 11:31:54 2004 by doxygen 1.3.8 written by Dimitri van Heesch, © 1997-2003