/* Copyright (C) 2001, S.R.Haque . Copyright (C) 2002, David Faure Copyright (C) 2004, Arend van Beelen jr. This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfind.h" #include "kfinddialog.h" #include #include #include #include #include #include #include #include #include //#define DEBUG_FIND #define INDEX_NOMATCH -1 class KFindNextDialog : public KDialogBase { public: KFindNextDialog(const TQString &pattern, TQWidget *parent); }; // Create the dialog. KFindNextDialog::KFindNextDialog(const TQString &pattern, TQWidget *parent) : KDialogBase(parent, 0, false, // non-modal! i18n("Find Next"), User1 | Close, User1, false, KStdGuiItem::find()) { setMainWidget( new TQLabel( i18n("Find next occurrence of '%1'?").arg(pattern), this ) ); } //// struct KFind::Private { Private() : findDialog(0), patternChanged(false), matchedPattern(""), incrementalPath(29, true), emptyMatch(0), currentId(0), customIds(false) { incrementalPath.setAutoDelete(true); data.setAutoDelete(true); } ~Private() { delete emptyMatch; emptyMatch = 0; } struct Match { Match(int dataId, int index, int matchedLength) : dataId(dataId), index(index), matchedLength(matchedLength) { } int dataId; int index; int matchedLength; }; struct Data { Data() : id(-1), dirty(false) { } Data(int id, const TQString &text, bool dirty = false) : id(id), text(text), dirty(dirty) { } int id; TQString text; bool dirty; }; TQGuardedPtr findDialog; bool patternChanged; TQString matchedPattern; TQDict incrementalPath; Match * emptyMatch; TQPtrVector data; int currentId; bool customIds; }; //// KFind::KFind( const TQString &pattern, long options, TQWidget *parent ) : TQObject( parent ) { d = new KFind::Private; m_options = options; init( pattern ); } KFind::KFind( const TQString &pattern, long options, TQWidget *parent, TQWidget *findDialog ) : TQObject( parent ) { d = new KFind::Private; d->findDialog = findDialog; m_options = options; init( pattern ); } void KFind::init( const TQString& pattern ) { m_matches = 0; m_pattern = pattern; m_dialog = 0; m_dialogClosed = false; m_index = INDEX_NOMATCH; m_lastResult = NoMatch; if (m_options & KFindDialog::RegularExpression) m_regExp = new TQRegExp(pattern, m_options & KFindDialog::CaseSensitive); else { m_regExp = 0; } } KFind::~KFind() { delete m_dialog; delete d; } bool KFind::needData() const { // always true when m_text is empty. if (m_options & KFindDialog::FindBackwards) // m_index==-1 and m_lastResult==Match means we haven't answered nomatch yet // This is important in the "replace with a prompt" case. return ( m_index < 0 && m_lastResult != Match ); else // "index over length" test removed: we want to get a nomatch before we set data again // This is important in the "replace with a prompt" case. return m_index == INDEX_NOMATCH; } void KFind::setData( const TQString& data, int startPos ) { setData( -1, data, startPos ); } void KFind::setData( int id, const TQString& data, int startPos ) { // cache the data for incremental find if ( m_options & KFindDialog::FindIncremental ) { if ( id != -1 ) d->customIds = true; else id = d->currentId + 1; if ( id >= (int) d->data.size() ) d->data.resize( id + 100 ); d->data.insert( id, new Private::Data(id, data, true) ); } if ( !(m_options & KFindDialog::FindIncremental) || needData() ) { m_text = data; if ( startPos != -1 ) m_index = startPos; else if (m_options & KFindDialog::FindBackwards) m_index = m_text.length(); else m_index = 0; #ifdef DEBUG_FIND kdDebug() << "setData: '" << m_text << "' m_index=" << m_index << endl; #endif Q_ASSERT( m_index != INDEX_NOMATCH ); m_lastResult = NoMatch; d->currentId = id; } } KDialogBase* KFind::findNextDialog( bool create ) { if ( !m_dialog && create ) { m_dialog = new KFindNextDialog( m_pattern, parentWidget() ); connect( m_dialog, TQT_SIGNAL( user1Clicked() ), this, TQT_SLOT( slotFindNext() ) ); connect( m_dialog, TQT_SIGNAL( finished() ), this, TQT_SLOT( slotDialogClosed() ) ); } return m_dialog; } KFind::Result KFind::find() { Q_ASSERT( m_index != INDEX_NOMATCH || d->patternChanged ); if ( m_lastResult == Match && !d->patternChanged ) { // Move on before looking for the next match, _if_ we just found a match if (m_options & KFindDialog::FindBackwards) { m_index--; if ( m_index == -1 ) // don't call KFind::find with -1, it has a special meaning { m_lastResult = NoMatch; return NoMatch; } } else m_index++; } d->patternChanged = false; if ( m_options & KFindDialog::FindIncremental ) { // if the current pattern is shorter than the matchedPattern we can // probably look up the match in the incrementalPath if ( m_pattern.length() < d->matchedPattern.length() ) { Private::Match *match = m_pattern.isEmpty() ? d->emptyMatch : d->incrementalPath[m_pattern]; TQString previousPattern = d->matchedPattern; d->matchedPattern = m_pattern; if ( match != 0 ) { bool clean = true; // find the first result backwards on the path that isn't dirty while ( d->data[match->dataId]->dirty == true && !m_pattern.isEmpty() ) { m_pattern.truncate( m_pattern.length() - 1 ); match = d->incrementalPath[m_pattern]; clean = false; } // remove all matches that lie after the current match while ( m_pattern.length() < previousPattern.length() ) { d->incrementalPath.remove(previousPattern); previousPattern.truncate(previousPattern.length() - 1); } // set the current text, index, etc. to the found match m_text = d->data[match->dataId]->text; m_index = match->index; m_matchedLength = match->matchedLength; d->currentId = match->dataId; // if the result is clean we can return it now if ( clean ) { if ( d->customIds ) emit highlight(d->currentId, m_index, m_matchedLength); else emit highlight(m_text, m_index, m_matchedLength); m_lastResult = Match; d->matchedPattern = m_pattern; return Match; } } // if we couldn't look up the match, the new pattern isn't a // substring of the matchedPattern, so we start a new search else { startNewIncrementalSearch(); } } // if the new pattern is longer than the matchedPattern we might be // able to proceed from the last search else if ( m_pattern.length() > d->matchedPattern.length() ) { // continue from the previous pattern if ( m_pattern.startsWith(d->matchedPattern) ) { // we can't proceed from the previous position if the previous // position already failed if ( m_index == INDEX_NOMATCH ) return NoMatch; TQString temp = m_pattern; m_pattern.truncate(d->matchedPattern.length() + 1); d->matchedPattern = temp; } // start a new search else { startNewIncrementalSearch(); } } // if the new pattern is as long as the matchedPattern, we reset if // they are not equal else if ( m_pattern != d->matchedPattern ) { startNewIncrementalSearch(); } } #ifdef DEBUG_FIND kdDebug() << k_funcinfo << "m_index=" << m_index << endl; #endif do { // if we have multiple data blocks in our cache, walk through these // blocks till we either searched all blocks or we find a match do { // Find the next candidate match. if ( m_options & KFindDialog::RegularExpression ) m_index = KFind::find(m_text, *m_regExp, m_index, m_options, &m_matchedLength); else m_index = KFind::find(m_text, m_pattern, m_index, m_options, &m_matchedLength); if ( m_options & KFindDialog::FindIncremental ) d->data[d->currentId]->dirty = false; if ( m_index == -1 && d->currentId < (int) d->data.count() - 1 ) { m_text = d->data[++d->currentId]->text; if ( m_options & KFindDialog::FindBackwards ) m_index = m_text.length(); else m_index = 0; } else break; } while ( !(m_options & KFindDialog::RegularExpression) ); if ( m_index != -1 ) { // Flexibility: the app can add more rules to validate a possible match if ( validateMatch( m_text, m_index, m_matchedLength ) ) { bool done = true; if ( m_options & KFindDialog::FindIncremental ) { if ( m_pattern.isEmpty() ) { delete d->emptyMatch; d->emptyMatch = new Private::Match( d->currentId, m_index, m_matchedLength ); } else d->incrementalPath.replace(m_pattern, new Private::Match(d->currentId, m_index, m_matchedLength)); if ( m_pattern.length() < d->matchedPattern.length() ) { m_pattern += d->matchedPattern.mid(m_pattern.length(), 1); done = false; } } if ( done ) { m_matches++; // Tell the world about the match we found, in case someone wants to // highlight it. if ( d->customIds ) emit highlight(d->currentId, m_index, m_matchedLength); else emit highlight(m_text, m_index, m_matchedLength); if ( !m_dialogClosed ) findNextDialog(true)->show(); #ifdef DEBUG_FIND kdDebug() << k_funcinfo << "Match. Next m_index=" << m_index << endl; #endif m_lastResult = Match; return Match; } } else // Skip match { if (m_options & KFindDialog::FindBackwards) m_index--; else m_index++; } } else { if ( m_options & KFindDialog::FindIncremental ) { TQString temp = m_pattern; temp.truncate(temp.length() - 1); m_pattern = d->matchedPattern; d->matchedPattern = temp; } m_index = INDEX_NOMATCH; } } while (m_index != INDEX_NOMATCH); #ifdef DEBUG_FIND kdDebug() << k_funcinfo << "NoMatch. m_index=" << m_index << endl; #endif m_lastResult = NoMatch; return NoMatch; } void KFind::startNewIncrementalSearch() { Private::Match *match = d->emptyMatch; if(match == 0) { m_text = TQString::null; m_index = 0; d->currentId = 0; } else { m_text = d->data[match->dataId]->text; m_index = match->index; d->currentId = match->dataId; } m_matchedLength = 0; d->incrementalPath.clear(); delete d->emptyMatch; d->emptyMatch = 0; d->matchedPattern = m_pattern; m_pattern = TQString::null; } // static int KFind::find(const TQString &text, const TQString &pattern, int index, long options, int *matchedLength) { // Handle regular expressions in the appropriate way. if (options & KFindDialog::RegularExpression) { TQRegExp regExp(pattern, options & KFindDialog::CaseSensitive); return find(text, regExp, index, options, matchedLength); } bool caseSensitive = (options & KFindDialog::CaseSensitive); if (options & KFindDialog::WholeWordsOnly) { if (options & KFindDialog::FindBackwards) { // Backward search, until the beginning of the line... while (index >= 0) { // ...find the next match. index = text.findRev(pattern, index, caseSensitive); if (index == -1) break; // Is the match delimited correctly? *matchedLength = pattern.length(); if (isWholeWords(text, index, *matchedLength)) break; index--; } } else { // Forward search, until the end of the line... while (index < (int)text.length()) { // ...find the next match. index = text.find(pattern, index, caseSensitive); if (index == -1) break; // Is the match delimited correctly? *matchedLength = pattern.length(); if (isWholeWords(text, index, *matchedLength)) break; index++; } if (index >= (int)text.length()) // end of line index = -1; // not found } } else { // Non-whole-word search. if (options & KFindDialog::FindBackwards) { index = text.findRev(pattern, index, caseSensitive); } else { index = text.find(pattern, index, caseSensitive); } if (index != -1) { *matchedLength = pattern.length(); } } return index; } // static int KFind::find(const TQString &text, const TQRegExp &pattern, int index, long options, int *matchedLength) { if (options & KFindDialog::WholeWordsOnly) { if (options & KFindDialog::FindBackwards) { // Backward search, until the beginning of the line... while (index >= 0) { // ...find the next match. index = text.findRev(pattern, index); if (index == -1) break; // Is the match delimited correctly? //pattern.match(text, index, matchedLength, false); /*int pos =*/ pattern.search( text.mid(index) ); *matchedLength = pattern.matchedLength(); if (isWholeWords(text, index, *matchedLength)) break; index--; } } else { // Forward search, until the end of the line... while (index < (int)text.length()) { // ...find the next match. index = text.find(pattern, index); if (index == -1) break; // Is the match delimited correctly? //pattern.match(text, index, matchedLength, false); /*int pos =*/ pattern.search( text.mid(index) ); *matchedLength = pattern.matchedLength(); if (isWholeWords(text, index, *matchedLength)) break; index++; } if (index >= (int)text.length()) // end of line index = -1; // not found } } else { // Non-whole-word search. if (options & KFindDialog::FindBackwards) { index = text.findRev(pattern, index); } else { index = text.find(pattern, index); } if (index != -1) { //pattern.match(text, index, matchedLength, false); /*int pos =*/ pattern.search( text.mid(index) ); *matchedLength = pattern.matchedLength(); } } return index; } bool KFind::isInWord(TQChar ch) { return ch.isLetter() || ch.isDigit() || ch == '_'; } bool KFind::isWholeWords(const TQString &text, int starts, int matchedLength) { if ((starts == 0) || (!isInWord(text[starts - 1]))) { int ends = starts + matchedLength; if ((ends == (int)text.length()) || (!isInWord(text[ends]))) return true; } return false; } void KFind::slotFindNext() { emit findNext(); } void KFind::slotDialogClosed() { emit dialogClosed(); m_dialogClosed = true; } void KFind::displayFinalDialog() const { TQString message; if ( numMatches() ) message = i18n( "1 match found.", "%n matches found.", numMatches() ); else message = i18n("No matches found for '%1'.").arg(TQStyleSheet::escape(m_pattern)); KMessageBox::information(dialogsParent(), message); } bool KFind::shouldRestart( bool forceAsking, bool showNumMatches ) const { // Only ask if we did a "find from cursor", otherwise it's pointless. // Well, unless the user can modify the document during a search operation, // hence the force boolean. if ( !forceAsking && (m_options & KFindDialog::FromCursor) == 0 ) { displayFinalDialog(); return false; } TQString message; if ( showNumMatches ) { if ( numMatches() ) message = i18n( "1 match found.", "%n matches found.", numMatches() ); else message = i18n("No matches found for '%1'.").arg(TQStyleSheet::escape(m_pattern)); } else { if ( m_options & KFindDialog::FindBackwards ) message = i18n( "Beginning of document reached." ); else message = i18n( "End of document reached." ); } message += "

"; // can't be in the i18n() of the first if() because of the plural form. // Hope this word puzzle is ok, it's a different sentence message += ( m_options & KFindDialog::FindBackwards ) ? i18n("Continue from the end?") : i18n("Continue from the beginning?"); int ret = KMessageBox::questionYesNo( dialogsParent(), TQString("")+message+TQString(""), TQString::null, KStdGuiItem::cont(), KStdGuiItem::stop() ); bool yes = ( ret == KMessageBox::Yes ); if ( yes ) const_cast(this)->m_options &= ~KFindDialog::FromCursor; // clear FromCursor option return yes; } void KFind::setOptions( long options ) { m_options = options; delete m_regExp; if (m_options & KFindDialog::RegularExpression) m_regExp = new TQRegExp(m_pattern, m_options & KFindDialog::CaseSensitive); else m_regExp = 0; } void KFind::closeFindNextDialog() { delete m_dialog; m_dialog = 0L; m_dialogClosed = true; } int KFind::index() const { return m_index; } void KFind::setPattern( const TQString& pattern ) { if ( m_options & KFindDialog::FindIncremental && m_pattern != pattern ) d->patternChanged = true; m_pattern = pattern; setOptions( options() ); // rebuild m_regExp if necessary } TQWidget* KFind::dialogsParent() const { // If the find dialog is still up, it should get the focus when closing a message box // Otherwise, maybe the "find next?" dialog is up // Otherwise, the "view" is the parent. return d->findDialog ? (TQWidget*)d->findDialog : ( m_dialog ? m_dialog : parentWidget() ); } #include "kfind.moc"