diff options
Diffstat (limited to 'kdecore/kcompletion.cpp')
-rw-r--r-- | kdecore/kcompletion.cpp | 892 |
1 files changed, 892 insertions, 0 deletions
diff --git a/kdecore/kcompletion.cpp b/kdecore/kcompletion.cpp new file mode 100644 index 000000000..dfa8f351b --- /dev/null +++ b/kdecore/kcompletion.cpp @@ -0,0 +1,892 @@ +/* This file is part of the KDE libraries + Copyright (C) 1999,2000,2001 Carsten Pfeiffer <pfeiffer@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + 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 <kapplication.h> +#include <kdebug.h> +#include <klocale.h> +#include <knotifyclient.h> +#include <kglobal.h> + +#include <qptrvector.h> + +#include "kcompletion.h" +#include "kcompletion_private.h" + + +class KCompletionPrivate +{ +public: + // not a member to avoid #including kcompletion_private.h from kcompletion.h + // list used for nextMatch() and previousMatch() + KCompletionMatchesWrapper matches; +}; + +KCompletion::KCompletion() +{ + d = new KCompletionPrivate; + + myCompletionMode = KGlobalSettings::completionMode(); + myTreeRoot = new KCompTreeNode; + myBeep = true; + myIgnoreCase = false; + myHasMultipleMatches = false; + myRotationIndex = 0; + setOrder( Insertion ); +} + +KCompletion::~KCompletion() +{ + delete d; + delete myTreeRoot; +} + +void KCompletion::setOrder( CompOrder order ) +{ + myOrder = order; + d->matches.setSorting( order == Weighted ); +} + +void KCompletion::setIgnoreCase( bool ignoreCase ) +{ + myIgnoreCase = ignoreCase; +} + +void KCompletion::setItems( const QStringList& items ) +{ + clear(); + insertItems( items ); +} + + +void KCompletion::insertItems( const QStringList& items ) +{ + bool weighted = (myOrder == Weighted); + QStringList::ConstIterator it; + if ( weighted ) { // determine weight + for ( it = items.begin(); it != items.end(); ++it ) + addWeightedItem( *it ); + } + else { + for ( it = items.begin(); it != items.end(); ++it ) + addItem( *it, 0 ); + } +} + +QStringList KCompletion::items() const +{ + KCompletionMatchesWrapper list; // unsorted + bool addWeight = (myOrder == Weighted); + extractStringsFromNode( myTreeRoot, QString::null, &list, addWeight ); + + return list.list(); +} + +bool KCompletion::isEmpty() const +{ + return (myTreeRoot->childrenCount() == 0); +} + +void KCompletion::addItem( const QString& item ) +{ + d->matches.clear(); + myRotationIndex = 0; + myLastString = QString::null; + + addItem( item, 0 ); +} + +void KCompletion::addItem( const QString& item, uint weight ) +{ + if ( item.isEmpty() ) + return; + + KCompTreeNode *node = myTreeRoot; + uint len = item.length(); + + bool sorted = (myOrder == Sorted); + bool weighted = ((myOrder == Weighted) && weight > 1); + + // knowing the weight of an item, we simply add this weight to all of its + // nodes. + + for ( uint i = 0; i < len; i++ ) { + node = node->insert( item.at(i), sorted ); + if ( weighted ) + node->confirm( weight -1 ); // node->insert() sets weighting to 1 + } + + // add 0x0-item as delimiter with evtl. weight + node = node->insert( 0x0, true ); + if ( weighted ) + node->confirm( weight -1 ); +// qDebug("*** added: %s (%i)", item.latin1(), node->weight()); +} + +void KCompletion::addWeightedItem( const QString& item ) +{ + if ( myOrder != Weighted ) { + addItem( item, 0 ); + return; + } + + uint len = item.length(); + uint weight = 0; + + // find out the weighting of this item (appended to the string as ":num") + int index = item.findRev(':'); + if ( index > 0 ) { + bool ok; + weight = item.mid( index + 1 ).toUInt( &ok ); + if ( !ok ) + weight = 0; + + len = index; // only insert until the ':' + } + + addItem( item.left( len ), weight ); + return; +} + + +void KCompletion::removeItem( const QString& item ) +{ + d->matches.clear(); + myRotationIndex = 0; + myLastString = QString::null; + + myTreeRoot->remove( item ); +} + + +void KCompletion::clear() +{ + d->matches.clear(); + myRotationIndex = 0; + myLastString = QString::null; + + delete myTreeRoot; + myTreeRoot = new KCompTreeNode; +} + + +QString KCompletion::makeCompletion( const QString& string ) +{ + if ( myCompletionMode == KGlobalSettings::CompletionNone ) + return QString::null; + + //kdDebug(0) << "KCompletion: completing: " << string << endl; + + d->matches.clear(); + myRotationIndex = 0; + myHasMultipleMatches = false; + myLastMatch = myCurrentMatch; + + // in Shell-completion-mode, emit all matches when we get the same + // complete-string twice + if ( myCompletionMode == KGlobalSettings::CompletionShell && + string == myLastString ) { + // Don't use d->matches since calling postProcessMatches() + // on d->matches here would interfere with call to + // postProcessMatch() during rotation + + findAllCompletions( string, &d->matches, myHasMultipleMatches ); + QStringList l = d->matches.list(); + postProcessMatches( &l ); + emit matches( l ); + + if ( l.isEmpty() ) + doBeep( NoMatch ); + + return QString::null; + } + + QString completion; + // in case-insensitive popup mode, we search all completions at once + if ( myCompletionMode == KGlobalSettings::CompletionPopup || + myCompletionMode == KGlobalSettings::CompletionPopupAuto ) { + findAllCompletions( string, &d->matches, myHasMultipleMatches ); + if ( !d->matches.isEmpty() ) + completion = d->matches.first(); + } + else + completion = findCompletion( string ); + + if ( myHasMultipleMatches ) + emit multipleMatches(); + + myLastString = string; + myCurrentMatch = completion; + + postProcessMatch( &completion ); + + if ( !string.isEmpty() ) { // only emit match when string is not empty + //kdDebug(0) << "KCompletion: Match: " << completion << endl; + emit match( completion ); + } + + if ( completion.isNull() ) + doBeep( NoMatch ); + + return completion; +} + + +QStringList KCompletion::substringCompletion( const QString& string ) const +{ + // get all items in the tree, possibly in sorted order + bool sorted = (myOrder == Weighted); + KCompletionMatchesWrapper allItems( sorted ); + extractStringsFromNode( myTreeRoot, QString::null, &allItems, false ); + + QStringList list = allItems.list(); + + // subStringMatches is invoked manually, via a shortcut, so we should + // beep here, if necessary. + if ( list.isEmpty() ) { + doBeep( NoMatch ); + return list; + } + + if ( string.isEmpty() ) { // shortcut + postProcessMatches( &list ); + return list; + } + + QStringList matches; + QStringList::ConstIterator it = list.begin(); + + for( ; it != list.end(); ++it ) { + QString item = *it; + if ( item.find( string, 0, false ) != -1 ) { // always case insensitive + matches.append( item ); + } + } + + postProcessMatches( &matches ); + + if ( matches.isEmpty() ) + doBeep( NoMatch ); + + return matches; +} + + +void KCompletion::setCompletionMode( KGlobalSettings::Completion mode ) +{ + myCompletionMode = mode; +} + +QStringList KCompletion::allMatches() +{ + // Don't use d->matches since calling postProcessMatches() + // on d->matches here would interfere with call to + // postProcessMatch() during rotation + KCompletionMatchesWrapper matches( myOrder == Weighted ); + bool dummy; + findAllCompletions( myLastString, &matches, dummy ); + QStringList l = matches.list(); + postProcessMatches( &l ); + return l; +} + +KCompletionMatches KCompletion::allWeightedMatches() +{ + // Don't use d->matches since calling postProcessMatches() + // on d->matches here would interfere with call to + // postProcessMatch() during rotation + KCompletionMatchesWrapper matches( myOrder == Weighted ); + bool dummy; + findAllCompletions( myLastString, &matches, dummy ); + KCompletionMatches ret( matches ); + postProcessMatches( &ret ); + return ret; +} + +QStringList KCompletion::allMatches( const QString &string ) +{ + KCompletionMatchesWrapper matches( myOrder == Weighted ); + bool dummy; + findAllCompletions( string, &matches, dummy ); + QStringList l = matches.list(); + postProcessMatches( &l ); + return l; +} + +KCompletionMatches KCompletion::allWeightedMatches( const QString &string ) +{ + KCompletionMatchesWrapper matches( myOrder == Weighted ); + bool dummy; + findAllCompletions( string, &matches, dummy ); + KCompletionMatches ret( matches ); + postProcessMatches( &ret ); + return ret; +} + +///////////////////////////////////////////////////// +///////////////// tree operations /////////////////// + + +QString KCompletion::nextMatch() +{ + QString completion; + myLastMatch = myCurrentMatch; + + if ( d->matches.isEmpty() ) { + findAllCompletions( myLastString, &d->matches, myHasMultipleMatches ); + completion = d->matches.first(); + myCurrentMatch = completion; + myRotationIndex = 0; + postProcessMatch( &completion ); + emit match( completion ); + return completion; + } + + QStringList matches = d->matches.list(); + myLastMatch = matches[ myRotationIndex++ ]; + + if ( myRotationIndex == matches.count() -1 ) + doBeep( Rotation ); // indicate last matching item -> rotating + + else if ( myRotationIndex == matches.count() ) + myRotationIndex = 0; + + completion = matches[ myRotationIndex ]; + myCurrentMatch = completion; + postProcessMatch( &completion ); + emit match( completion ); + return completion; +} + + + +QString KCompletion::previousMatch() +{ + QString completion; + myLastMatch = myCurrentMatch; + + if ( d->matches.isEmpty() ) { + findAllCompletions( myLastString, &d->matches, myHasMultipleMatches ); + completion = d->matches.last(); + myCurrentMatch = completion; + myRotationIndex = 0; + postProcessMatch( &completion ); + emit match( completion ); + return completion; + } + + QStringList matches = d->matches.list(); + myLastMatch = matches[ myRotationIndex ]; + if ( myRotationIndex == 1 ) + doBeep( Rotation ); // indicate first item -> rotating + + else if ( myRotationIndex == 0 ) + myRotationIndex = matches.count(); + + myRotationIndex--; + + completion = matches[ myRotationIndex ]; + myCurrentMatch = completion; + postProcessMatch( &completion ); + emit match( completion ); + return completion; +} + + + +// tries to complete "string" from the tree-root +QString KCompletion::findCompletion( const QString& string ) +{ + QChar ch; + QString completion; + const KCompTreeNode *node = myTreeRoot; + + // start at the tree-root and try to find the search-string + for( uint i = 0; i < string.length(); i++ ) { + ch = string.at( i ); + node = node->find( ch ); + + if ( node ) + completion += ch; + else + return QString::null; // no completion + } + + // Now we have the last node of the to be completed string. + // Follow it as long as it has exactly one child (= longest possible + // completion) + + while ( node->childrenCount() == 1 ) { + node = node->firstChild(); + if ( !node->isNull() ) + completion += *node; + } + // if multiple matches and auto-completion mode + // -> find the first complete match + if ( node && node->childrenCount() > 1 ) { + myHasMultipleMatches = true; + + if ( myCompletionMode == KGlobalSettings::CompletionAuto ) { + myRotationIndex = 1; + if (myOrder != Weighted) { + while ( (node = node->firstChild()) ) { + if ( !node->isNull() ) + completion += *node; + else + break; + } + } + else { + // don't just find the "first" match, but the one with the + // highest priority + + const KCompTreeNode* temp_node = 0L; + while(1) { + int count = node->childrenCount(); + temp_node = node->firstChild(); + uint weight = temp_node->weight(); + const KCompTreeNode* hit = temp_node; + for( int i = 1; i < count; i++ ) { + temp_node = node->childAt(i); + if( temp_node->weight() > weight ) { + hit = temp_node; + weight = hit->weight(); + } + } + // 0x0 has the highest priority -> we have the best match + if ( hit->isNull() ) + break; + + node = hit; + completion += *node; + } + } + } + + else + doBeep( PartialMatch ); // partial match -> beep + } + + return completion; +} + + +void KCompletion::findAllCompletions(const QString& string, + KCompletionMatchesWrapper *matches, + bool& hasMultipleMatches) const +{ + //kdDebug(0) << "*** finding all completions for " << string << endl; + + if ( string.isEmpty() ) + return; + + if ( myIgnoreCase ) { // case insensitive completion + extractStringsFromNodeCI( myTreeRoot, QString::null, string, matches ); + hasMultipleMatches = (matches->count() > 1); + return; + } + + QChar ch; + QString completion; + const KCompTreeNode *node = myTreeRoot; + + // start at the tree-root and try to find the search-string + for( uint i = 0; i < string.length(); i++ ) { + ch = string.at( i ); + node = node->find( ch ); + + if ( node ) + completion += ch; + else + return; // no completion -> return empty list + } + + // Now we have the last node of the to be completed string. + // Follow it as long as it has exactly one child (= longest possible + // completion) + + while ( node->childrenCount() == 1 ) { + node = node->firstChild(); + if ( !node->isNull() ) + completion += *node; + // kdDebug() << completion << node->latin1(); + } + + + // there is just one single match) + if ( node->childrenCount() == 0 ) + matches->append( node->weight(), completion ); + + else { + // node has more than one child + // -> recursively find all remaining completions + hasMultipleMatches = true; + extractStringsFromNode( node, completion, matches ); + } +} + + +void KCompletion::extractStringsFromNode( const KCompTreeNode *node, + const QString& beginning, + KCompletionMatchesWrapper *matches, + bool addWeight ) const +{ + if ( !node || !matches ) + return; + + // kDebug() << "Beginning: " << beginning << endl; + const KCompTreeChildren *list = node->children(); + QString string; + QString w; + + // loop thru all children + for ( KCompTreeNode *cur = list->begin(); cur ; cur = cur->next) { + string = beginning; + node = cur; + if ( !node->isNull() ) + string += *node; + + while ( node && node->childrenCount() == 1 ) { + node = node->firstChild(); + if ( node->isNull() ) + break; + string += *node; + } + + if ( node && node->isNull() ) { // we found a leaf + if ( addWeight ) { + // add ":num" to the string to store the weighting + string += ':'; + w.setNum( node->weight() ); + string.append( w ); + } + matches->append( node->weight(), string ); + } + + // recursively find all other strings. + if ( node && node->childrenCount() > 1 ) + extractStringsFromNode( node, string, matches, addWeight ); + } +} + +void KCompletion::extractStringsFromNodeCI( const KCompTreeNode *node, + const QString& beginning, + const QString& restString, + KCompletionMatchesWrapper *matches ) const +{ + if ( restString.isEmpty() ) { + extractStringsFromNode( node, beginning, matches, false /*noweight*/ ); + return; + } + + QChar ch1 = restString.at(0); + QString newRest = restString.mid(1); + KCompTreeNode *child1, *child2; + + child1 = node->find( ch1 ); // the correct match + if ( child1 ) + extractStringsFromNodeCI( child1, beginning + *child1, newRest, + matches ); + + // append the case insensitive matches, if available + if ( ch1.isLetter() ) { + // find out if we have to lower or upper it. Is there a better way? + QChar ch2 = ch1.lower(); + if ( ch1 == ch2 ) + ch2 = ch1.upper(); + if ( ch1 != ch2 ) { + child2 = node->find( ch2 ); + if ( child2 ) + extractStringsFromNodeCI( child2, beginning + *child2, newRest, + matches ); + } + } +} + +void KCompletion::doBeep( BeepMode mode ) const +{ + if ( !myBeep ) + return; + + QString text, event; + + switch ( mode ) { + case Rotation: + event = QString::fromLatin1("Textcompletion: rotation"); + text = i18n("You reached the end of the list\nof matching items.\n"); + break; + case PartialMatch: + if ( myCompletionMode == KGlobalSettings::CompletionShell || + myCompletionMode == KGlobalSettings::CompletionMan ) { + event = QString::fromLatin1("Textcompletion: partial match"); + text = i18n("The completion is ambiguous, more than one\nmatch is available.\n"); + } + break; + case NoMatch: + if ( myCompletionMode == KGlobalSettings::CompletionShell ) { + event = QString::fromLatin1("Textcompletion: no match"); + text = i18n("There is no matching item available.\n"); + } + break; + } + + if ( !text.isEmpty() ) + KNotifyClient::event( event, text ); +} + + +///////////////////////////////// +///////// + + +// Implements the tree. Every node is a QChar and has a list of children, which +// are Nodes as well. +// QChar( 0x0 ) is used as the delimiter of a string; the last child of each +// inserted string is 0x0. + +KCompTreeNode::~KCompTreeNode() +{ + // delete all children + KCompTreeNode *cur = myChildren.begin(); + while (cur) { + KCompTreeNode * next = cur->next; + delete myChildren.remove(cur); + cur = next; + } +} + + +// Adds a child-node "ch" to this node. If such a node is already existant, +// it will not be created. Returns the new/existing node. +KCompTreeNode * KCompTreeNode::insert( const QChar& ch, bool sorted ) +{ + KCompTreeNode *child = find( ch ); + if ( !child ) { + child = new KCompTreeNode( ch ); + + // FIXME, first (slow) sorted insertion implementation + if ( sorted ) { + KCompTreeNode * prev = 0; + KCompTreeNode * cur = myChildren.begin(); + while ( cur ) { + if ( ch > *cur ) { + prev = cur; + cur = cur->next; + } else + break; + } + if (prev) + myChildren.insert( prev, child ); + else + myChildren.prepend(child); + } + + else + myChildren.append( child ); + } + + // implicit weighting: the more often an item is inserted, the higher + // priority it gets. + child->confirm(); + + return child; +} + + +// Iteratively removes a string from the tree. The nicer recursive +// version apparently was a little memory hungry (see #56757) +void KCompTreeNode::remove( const QString& str ) +{ + QString string = str; + string += QChar(0x0); + + QPtrVector<KCompTreeNode> deletables( string.length() + 1 ); + + KCompTreeNode *child = 0L; + KCompTreeNode *parent = this; + deletables.insert( 0, parent ); + + uint i = 0; + for ( ; i < string.length(); i++ ) + { + child = parent->find( string.at( i ) ); + if ( child ) + deletables.insert( i + 1, child ); + else + break; + + parent = child; + } + + for ( ; i >= 1; i-- ) + { + parent = deletables.at( i - 1 ); + child = deletables.at( i ); + if ( child->myChildren.count() == 0 ) + delete parent->myChildren.remove( child ); + } +} + +QStringList KCompletionMatchesWrapper::list() const +{ + if ( sortedList && dirty ) { + sortedList->sort(); + dirty = false; + + stringList.clear(); + + // high weight == sorted last -> reverse the sorting here + QValueListConstIterator<KSortableItem<QString> > it; + for ( it = sortedList->begin(); it != sortedList->end(); ++it ) + stringList.prepend( (*it).value() ); + } + + return stringList; +} + +KCompletionMatches::KCompletionMatches( bool sort_P ) + : _sorting( sort_P ) +{ +} + +KCompletionMatches::KCompletionMatches( const KCompletionMatchesWrapper& matches ) + : _sorting( matches.sorting()) +{ + if( matches.sortedList != 0L ) + KCompletionMatchesList::operator=( *matches.sortedList ); + else { + QStringList l = matches.list(); + for( QStringList::ConstIterator it = l.begin(); + it != l.end(); + ++it ) + prepend( KSortableItem<QString, int>( 1, *it ) ); + } +} + +KCompletionMatches::~KCompletionMatches() +{ +} + +QStringList KCompletionMatches::list( bool sort_P ) const +{ + if( _sorting && sort_P ) + const_cast< KCompletionMatches* >( this )->sort(); + QStringList stringList; + // high weight == sorted last -> reverse the sorting here + for ( ConstIterator it = begin(); it != end(); ++it ) + stringList.prepend( (*it).value() ); + return stringList; +} + +void KCompletionMatches::removeDuplicates() +{ + Iterator it1, it2; + for ( it1 = begin(); it1 != end(); ++it1 ) { + for ( (it2 = it1), ++it2; it2 != end();) { + if( (*it1).value() == (*it2).value()) { + // use the max height + (*it1).first = kMax( (*it1).index(), (*it2).index()); + it2 = remove( it2 ); + continue; + } + ++it2; + } + } +} + +void KCompTreeNodeList::append(KCompTreeNode *item) +{ + m_count++; + if (!last) { + last = item; + last->next = 0; + first = item; + return; + } + last->next = item; + item->next = 0; + last = item; +} + +void KCompTreeNodeList::prepend(KCompTreeNode *item) +{ + m_count++; + if (!last) { + last = item; + last->next = 0; + first = item; + return; + } + item->next = first; + first = item; +} + +void KCompTreeNodeList::insert(KCompTreeNode *after, KCompTreeNode *item) +{ + if (!after) { + append(item); + return; + } + + m_count++; + + item->next = after->next; + after->next = item; + + if (after == last) + last = item; +} + +KCompTreeNode *KCompTreeNodeList::remove(KCompTreeNode *item) +{ + if (!first || !item) + return 0; + KCompTreeNode *cur = 0; + + if (item == first) + first = first->next; + else { + cur = first; + while (cur && cur->next != item) cur = cur->next; + if (!cur) + return 0; + cur->next = item->next; + } + if (item == last) + last = cur; + m_count--; + return item; +} + +KCompTreeNode *KCompTreeNodeList::at(uint index) const +{ + KCompTreeNode *cur = first; + while (index-- && cur) cur = cur->next; + return cur; +} + +KZoneAllocator KCompTreeNode::alloc(8192); + +void KCompletion::virtual_hook( int, void* ) +{ /*BASE::virtual_hook( id, data );*/ } + +void KCompletionBase::virtual_hook( int, void* ) +{ /*BASE::virtual_hook( id, data );*/ } + +#include "kcompletion.moc" |