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.4.0.
Documentation copyright © 1996-2004 the KDE developers.
Generated on Fri Jul 21 13:14:48 2006 by doxygen 1.4.0 written by Dimitri van Heesch, © 1997-2003