libkdepim Library API Documentation

addresseelineedit.cpp

00001 /*
00002     This file is part of libkdepim.
00003     Copyright (c) 2002 Helge Deller <deller@gmx.de>
00004                   2002 Lubos Lunak <llunak@suse.cz>
00005                   2001,2003 Carsten Pfeiffer <pfeiffer@kde.org>
00006                   2001 Waldo Bastian <bastian@kde.org>
00007                   2004 Daniel Molkentin <danimo@klaralvdalens-datakonsult.se>
00008                   2004 Karl-Heinz Zimmer <khz@klaralvdalens-datakonsult.se>
00009                   2005 Till Adam <till@klaralvdalens-datakonsult.se>
00010 
00011     This library is free software; you can redistribute it and/or
00012     modify it under the terms of the GNU Library General Public
00013     License as published by the Free Software Foundation; either
00014     version 2 of the License, or (at your option) any later version.
00015 
00016     This library is distributed in the hope that it will be useful,
00017     but WITHOUT ANY WARRANTY; without even the implied warranty of
00018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
00019     Library General Public License for more details.
00020 
00021     You should have received a copy of the GNU Library General Public License
00022     along with this library; see the file COPYING.LIB.  If not, write to
00023     the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
00024     Boston, MA 02110-1301, USA.
00025 */
00026 
00027 #include "addresseelineedit.h"
00028 
00029 #include "resourceabc.h"
00030 #include "completionordereditor.h"
00031 #include "ldapclient.h"
00032 
00033 #include "distributionlist.h"
00034 #include <kabc/distributionlist.h>
00035 #include <kabc/stdaddressbook.h>
00036 #include <kabc/resource.h>
00037 
00038 #include <kcompletionbox.h>
00039 #include <kcursor.h>
00040 #include <kdebug.h>
00041 #include <kstandarddirs.h>
00042 #include <kstaticdeleter.h>
00043 #include <kstdaccel.h>
00044 #include <kurldrag.h>
00045 #include <klocale.h>
00046 
00047 #include <qpopupmenu.h>
00048 #include <qapplication.h>
00049 #include <qobject.h>
00050 #include <qptrlist.h>
00051 #include <qregexp.h>
00052 #include <qevent.h>
00053 #include <qdragobject.h>
00054 #include <qclipboard.h>
00055 
00056 using namespace KPIM;
00057 
00058 KMailCompletion * AddresseeLineEdit::s_completion = 0L;
00059 KPIM::CompletionItemsMap* AddresseeLineEdit::s_completionItemMap = 0L;
00060 QStringList* AddresseeLineEdit::s_completionSources = 0L;
00061 bool AddresseeLineEdit::s_addressesDirty = false;
00062 QTimer* AddresseeLineEdit::s_LDAPTimer = 0L;
00063 KPIM::LdapSearch* AddresseeLineEdit::s_LDAPSearch = 0L;
00064 QString* AddresseeLineEdit::s_LDAPText = 0L;
00065 AddresseeLineEdit* AddresseeLineEdit::s_LDAPLineEdit = 0L;
00066 
00067 static KStaticDeleter<KMailCompletion> completionDeleter;
00068 static KStaticDeleter<KPIM::CompletionItemsMap> completionItemsDeleter;
00069 static KStaticDeleter<QTimer> ldapTimerDeleter;
00070 static KStaticDeleter<KPIM::LdapSearch> ldapSearchDeleter;
00071 static KStaticDeleter<QString> ldapTextDeleter;
00072 static KStaticDeleter<QStringList> completionSourcesDeleter;
00073 
00074 // needs to be unique, but the actual name doesn't matter much
00075 static QCString newLineEditDCOPObjectName()
00076 {
00077     static int s_count = 0;
00078     QCString name( "KPIM::AddresseeLineEdit" );
00079     if ( s_count++ ) {
00080       name += '-';
00081       name += QCString().setNum( s_count );
00082     }
00083     return name;
00084 }
00085 
00086 static const QString s_completionItemIndentString = "     ";
00087 
00088 static bool itemIsHeader( const QListBoxItem* item )
00089 {
00090   return item && !item->text().startsWith( s_completionItemIndentString );
00091 }
00092 
00093 
00094 
00095 AddresseeLineEdit::AddresseeLineEdit( QWidget* parent, bool useCompletion,
00096                                       const char *name )
00097   : ClickLineEdit( parent, QString::null, name ), DCOPObject( newLineEditDCOPObjectName() )
00098 {
00099   m_useCompletion = useCompletion;
00100   m_completionInitialized = false;
00101   m_smartPaste = false;
00102   m_addressBookConnected = false;
00103   m_searchExtended = false;
00104 
00105   init();
00106 
00107   if ( m_useCompletion )
00108     s_addressesDirty = true;
00109 }
00110 
00111 
00112 void AddresseeLineEdit::init()
00113 {
00114   if ( !s_completion ) {
00115     completionDeleter.setObject( s_completion, new KMailCompletion() );
00116     s_completion->setOrder( completionOrder() );
00117     s_completion->setIgnoreCase( true );
00118 
00119     completionItemsDeleter.setObject( s_completionItemMap, new KPIM::CompletionItemsMap() );
00120     completionSourcesDeleter.setObject( s_completionSources, new QStringList() );
00121   }
00122 
00123 //  connect( s_completion, SIGNAL( match( const QString& ) ),
00124 //           this, SLOT( slotMatched( const QString& ) ) );
00125 
00126   if ( m_useCompletion ) {
00127     if ( !s_LDAPTimer ) {
00128       ldapTimerDeleter.setObject( s_LDAPTimer, new QTimer( 0, "ldapTimerDeleter" ) );
00129       ldapSearchDeleter.setObject( s_LDAPSearch, new KPIM::LdapSearch );
00130       ldapTextDeleter.setObject( s_LDAPText, new QString );
00131 
00132       /* Add completion sources for all ldap server, 0 to n. Added first so
00133        * that they map to the ldapclient::clientNumber() */
00134       QValueList< LdapClient* > clients =  s_LDAPSearch->clients();
00135       for ( QValueList<LdapClient*>::iterator it = clients.begin(); it != clients.end(); ++it ) {
00136         addCompletionSource( "LDAP server: " + (*it)->host() );
00137       }
00138     }
00139     if ( !m_completionInitialized ) {
00140       setCompletionObject( s_completion, false );
00141       connect( this, SIGNAL( completion( const QString& ) ),
00142           this, SLOT( slotCompletion() ) );
00143       connect( this, SIGNAL( returnPressed( const QString& ) ),
00144           this, SLOT( slotReturnPressed() ) );
00145 
00146       KCompletionBox *box = completionBox();
00147       connect( box, SIGNAL( highlighted( const QString& ) ),
00148           this, SLOT( slotPopupCompletion( const QString& ) ) );
00149       connect( box, SIGNAL( userCancelled( const QString& ) ),
00150           SLOT( slotUserCancelled( const QString& ) ) );
00151 
00152       // The emitter is always called KPIM::IMAPCompletionOrder by contract
00153       if ( !connectDCOPSignal( 0, "KPIM::IMAPCompletionOrder", "orderChanged()",
00154             "slotIMAPCompletionOrderChanged()", false ) )
00155         kdError() << "AddresseeLineEdit: connection to orderChanged() failed" << endl;
00156 
00157       connect( s_LDAPTimer, SIGNAL( timeout() ), SLOT( slotStartLDAPLookup() ) );
00158       connect( s_LDAPSearch, SIGNAL( searchData( const KPIM::LdapResultList& ) ),
00159           SLOT( slotLDAPSearchData( const KPIM::LdapResultList& ) ) );
00160 
00161       m_completionInitialized = true;
00162     }
00163   }
00164 }
00165 
00166 AddresseeLineEdit::~AddresseeLineEdit()
00167 {
00168   if ( s_LDAPSearch && s_LDAPLineEdit == this )
00169     stopLDAPLookup();
00170 }
00171 
00172 void AddresseeLineEdit::setFont( const QFont& font )
00173 {
00174   KLineEdit::setFont( font );
00175   if ( m_useCompletion )
00176     completionBox()->setFont( font );
00177 }
00178 
00179 void AddresseeLineEdit::allowSemiColonAsSeparator( bool useSemiColonAsSeparator )
00180 {
00181   m_useSemiColonAsSeparator = useSemiColonAsSeparator;
00182 }
00183 
00184 void AddresseeLineEdit::keyPressEvent( QKeyEvent *e )
00185 {
00186   bool accept = false;
00187 
00188   if ( KStdAccel::shortcut( KStdAccel::SubstringCompletion ).contains( KKey( e ) ) ) {
00189     //TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch
00190     updateSearchString();
00191     doCompletion( true );
00192     accept = true;
00193   } else if ( KStdAccel::shortcut( KStdAccel::TextCompletion ).contains( KKey( e ) ) ) {
00194     int len = text().length();
00195 
00196     if ( len == cursorPosition() ) { // at End?
00197       updateSearchString();
00198       doCompletion( true );
00199       accept = true;
00200     }
00201   }
00202 
00203   if ( !accept )
00204     KLineEdit::keyPressEvent( e );
00205 
00206   if ( e->isAccepted() ) {
00207     updateSearchString();
00208     QString searchString( m_searchString );
00209     //LDAP does not know about our string manipulation, remove it
00210     if ( m_searchExtended )
00211         searchString = m_searchString.mid( 1 );
00212 
00213     if ( m_useCompletion && s_LDAPTimer != NULL ) {
00214       if ( *s_LDAPText != searchString || s_LDAPLineEdit != this )
00215         stopLDAPLookup();
00216 
00217       *s_LDAPText = searchString;
00218       s_LDAPLineEdit = this;
00219       s_LDAPTimer->start( 500, true );
00220     }
00221   }
00222 }
00223 
00224 void AddresseeLineEdit::insert( const QString &t )
00225 {
00226   if ( !m_smartPaste ) {
00227     KLineEdit::insert( t );
00228     return;
00229   }
00230 
00231   //kdDebug(5300) << "     AddresseeLineEdit::insert( \"" << t << "\" )" << endl;
00232 
00233   QString newText = t.stripWhiteSpace();
00234   if ( newText.isEmpty() )
00235     return;
00236 
00237   // remove newlines in the to-be-pasted string
00238   QStringList lines = QStringList::split( QRegExp("\r?\n"), newText, false );
00239   for ( QStringList::iterator it = lines.begin();
00240        it != lines.end(); ++it ) {
00241     // remove trailing commas and whitespace
00242     (*it).remove( QRegExp(",?\\s*$") );
00243   }
00244   newText = lines.join( ", " );
00245 
00246   if ( newText.lower().startsWith("mailto:") ) {
00247     KURL url( newText );
00248     newText = url.path();
00249   }
00250   else if ( newText.find(" at ") != -1 ) {
00251     // Anti-spam stuff
00252     newText.replace( " at ", "@" );
00253     newText.replace( " dot ", "." );
00254   }
00255   else if ( newText.find("(at)") != -1 ) {
00256     newText.replace( QRegExp("\\s*\\(at\\)\\s*"), "@" );
00257   }
00258 
00259   QString contents = text();
00260   int start_sel = 0;
00261   int end_sel = 0;
00262   int pos = cursorPosition();
00263   if ( getSelection( &start_sel, &end_sel ) ) {
00264     // Cut away the selection.
00265     if ( pos > end_sel )
00266       pos -= (end_sel - start_sel);
00267     else if ( pos > start_sel )
00268       pos = start_sel;
00269     contents = contents.left( start_sel ) + contents.right( end_sel + 1 );
00270   }
00271 
00272   int eot = contents.length();
00273   while ((eot > 0) && contents[ eot - 1 ].isSpace() ) eot--;
00274   if ( eot == 0 )
00275     contents = QString::null;
00276   else if ( pos >= eot ) {
00277     if ( contents[ eot - 1 ] == ',' )
00278       eot--;
00279     contents.truncate( eot );
00280     contents += ", ";
00281     pos = eot + 2;
00282   }
00283 
00284   contents = contents.left( pos ) + newText + contents.mid( pos );
00285   setText( contents );
00286   setEdited( true );
00287   setCursorPosition( pos + newText.length() );
00288 }
00289 
00290 void AddresseeLineEdit::setText( const QString & text )
00291 {
00292   ClickLineEdit::setText( text.stripWhiteSpace() );
00293 }
00294 
00295 void AddresseeLineEdit::paste()
00296 {
00297   if ( m_useCompletion )
00298     m_smartPaste = true;
00299 
00300   KLineEdit::paste();
00301   m_smartPaste = false;
00302 }
00303 
00304 void AddresseeLineEdit::mouseReleaseEvent( QMouseEvent *e )
00305 {
00306   // reimplemented from QLineEdit::mouseReleaseEvent()
00307   if ( m_useCompletion
00308        && QApplication::clipboard()->supportsSelection()
00309        && !isReadOnly()
00310        && e->button() == MidButton ) {
00311     m_smartPaste = true;
00312   }
00313 
00314   KLineEdit::mouseReleaseEvent( e );
00315   m_smartPaste = false;
00316 }
00317 
00318 void AddresseeLineEdit::dropEvent( QDropEvent *e )
00319 {
00320   KURL::List uriList;
00321   if ( !isReadOnly()
00322        && KURLDrag::canDecode(e) && KURLDrag::decode( e, uriList ) ) {
00323     QString contents = text();
00324     // remove trailing white space and comma
00325     int eot = contents.length();
00326     while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() )
00327       eot--;
00328     if ( eot == 0 )
00329       contents = QString::null;
00330     else if ( contents[ eot - 1 ] == ',' ) {
00331       eot--;
00332       contents.truncate( eot );
00333     }
00334     bool mailtoURL = false;
00335     // append the mailto URLs
00336     for ( KURL::List::Iterator it = uriList.begin();
00337           it != uriList.end(); ++it ) {
00338       if ( !contents.isEmpty() )
00339         contents.append( ", " );
00340       KURL u( *it );
00341       if ( u.protocol() == "mailto" ) {
00342         mailtoURL = true;
00343         contents.append( (*it).path() );
00344       }
00345     }
00346     if ( mailtoURL ) {
00347       setText( contents );
00348       setEdited( true );
00349       return;
00350     }
00351   }
00352 
00353   if ( m_useCompletion )
00354     m_smartPaste = true;
00355   QLineEdit::dropEvent( e );
00356   m_smartPaste = false;
00357 }
00358 
00359 void AddresseeLineEdit::cursorAtEnd()
00360 {
00361   setCursorPosition( text().length() );
00362 }
00363 
00364 void AddresseeLineEdit::enableCompletion( bool enable )
00365 {
00366   m_useCompletion = enable;
00367 }
00368 
00369 void AddresseeLineEdit::doCompletion( bool ctrlT )
00370 {
00371   m_lastSearchMode = ctrlT;
00372 
00373   KGlobalSettings::Completion  mode = completionMode();
00374 
00375   if ( mode == KGlobalSettings::CompletionNone  )
00376     return;
00377 
00378   if ( s_addressesDirty ) {
00379     loadContacts(); // read from local address book
00380     s_completion->setOrder( completionOrder() );
00381   }
00382 
00383   // cursor at end of string - or Ctrl+T pressed for substring completion?
00384   if ( ctrlT ) {
00385     const QStringList completions = getAdjustedCompletionItems( false );
00386 
00387     if ( completions.count() > 1 )
00388       ; //m_previousAddresses = prevAddr;
00389     else if ( completions.count() == 1 )
00390       setText( m_previousAddresses + completions.first().stripWhiteSpace() );
00391 
00392     setCompletedItems( completions, true ); // this makes sure the completion popup is closed if no matching items were found
00393 
00394     cursorAtEnd();
00395     setCompletionMode( mode ); //set back to previous mode
00396     return;
00397   }
00398 
00399 
00400   switch ( mode ) {
00401     case KGlobalSettings::CompletionPopupAuto:
00402     {
00403       if ( m_searchString.isEmpty() )
00404         break;
00405     }
00406 
00407     case KGlobalSettings::CompletionPopup:
00408     {
00409       const QStringList items = getAdjustedCompletionItems( true );
00410       setCompletedItems( items, false );
00411       break;
00412     }
00413 
00414     case KGlobalSettings::CompletionShell:
00415     {
00416       QString match = s_completion->makeCompletion( m_searchString );
00417       if ( !match.isNull() && match != m_searchString ) {
00418         setText( m_previousAddresses + match );
00419         setEdited( true );
00420         cursorAtEnd();
00421       }
00422       break;
00423     }
00424 
00425     case KGlobalSettings::CompletionMan: // Short-Auto in fact
00426     case KGlobalSettings::CompletionAuto:
00427     {
00428       //force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect
00429       setCompletionMode( completionMode() );
00430 
00431       if ( !m_searchString.isEmpty() ) {
00432 
00433         //if only our \" is left, remove it since user has not typed it either
00434         if ( m_searchExtended && m_searchString == "\"" ){
00435           m_searchExtended = false;
00436           m_searchString = QString::null;
00437           setText( m_previousAddresses );
00438           break;
00439         }
00440 
00441         QString match = s_completion->makeCompletion( m_searchString );
00442 
00443         if ( !match.isEmpty() ) {
00444           if ( match != m_searchString ) {
00445             QString adds = m_previousAddresses + match;
00446             setCompletedText( adds );
00447           }
00448         } else {
00449           if ( !m_searchString.startsWith( "\"" ) ) {
00450             //try with quoted text, if user has not type one already
00451             match = s_completion->makeCompletion( "\"" + m_searchString );
00452             if ( !match.isEmpty() && match != m_searchString ) {
00453               m_searchString = "\"" + m_searchString;
00454               m_searchExtended = true;
00455               setText( m_previousAddresses + m_searchString );
00456               setCompletedText( m_previousAddresses + match );
00457             }
00458           } else if ( m_searchExtended ) {
00459             //our added \" does not work anymore, remove it
00460             m_searchString = m_searchString.mid( 1 );
00461             m_searchExtended = false;
00462             setText( m_previousAddresses + m_searchString );
00463             //now try again
00464             match = s_completion->makeCompletion( m_searchString );
00465             if ( !match.isEmpty() && match != m_searchString ) {
00466               QString adds = m_previousAddresses + match;
00467               setCompletedText( adds );
00468             }
00469           }
00470         }
00471       }
00472       break;
00473     }
00474 
00475     case KGlobalSettings::CompletionNone:
00476     default: // fall through
00477       break;
00478   }
00479 }
00480 
00481 void AddresseeLineEdit::slotPopupCompletion( const QString& completion )
00482 {
00483   setText( m_previousAddresses + completion.stripWhiteSpace() );
00484   cursorAtEnd();
00485 //  slotMatched( m_previousAddresses + completion );
00486   updateSearchString();
00487 }
00488 
00489 void AddresseeLineEdit::slotReturnPressed()
00490 {
00491   QListBoxItem* i = completionBox()->selectedItem();
00492   if ( i != 0 )
00493     slotPopupCompletion( i->text() );
00494 }
00495 
00496 void AddresseeLineEdit::loadContacts()
00497 {
00498   s_completion->clear();
00499   s_completionItemMap->clear();
00500   s_addressesDirty = false;
00501   //m_contactMap.clear();
00502 
00503   QApplication::setOverrideCursor( KCursor::waitCursor() ); // loading might take a while
00504 
00505   KConfig config( "kpimcompletionorder" ); // The weights for non-imap kabc resources is there.
00506   config.setGroup( "CompletionWeights" );
00507 
00508   KABC::AddressBook *addressBook = KABC::StdAddressBook::self( true );
00509   // Can't just use the addressbook's iterator, we need to know which subresource
00510   // is behind which contact.
00511   QPtrList<KABC::Resource> resources( addressBook->resources() );
00512   for( QPtrListIterator<KABC::Resource> resit( resources ); *resit; ++resit ) {
00513     KABC::Resource* resource = *resit;
00514     KPIM::ResourceABC* resabc = dynamic_cast<ResourceABC *>( resource );
00515     if ( resabc ) { // IMAP KABC resource; need to associate each contact with the subresource
00516       const QMap<QString, QString> uidToResourceMap = resabc->uidToResourceMap();
00517       KABC::Resource::Iterator it;
00518       for ( it = resource->begin(); it != resource->end(); ++it ) {
00519         QString uid = (*it).uid();
00520         QMap<QString, QString>::const_iterator wit = uidToResourceMap.find( uid );
00521         const QString subresourceLabel = resabc->subresourceLabel( *wit );
00522         int idx = s_completionSources->findIndex( subresourceLabel );
00523         if ( idx == -1 ) {
00524           s_completionSources->append( subresourceLabel );
00525           idx = s_completionSources->size() -1;
00526         }
00527         int weight = ( wit != uidToResourceMap.end() ) ? resabc->subresourceCompletionWeight( *wit ) : 80;
00528         //kdDebug(5300) << (*it).fullEmail() << " subres=" << *wit << " weight=" << weight << endl;
00529         addContact( *it, weight, idx );
00530       }
00531     } else { // KABC non-imap resource
00532       int weight = config.readNumEntry( resource->identifier(), 60 );
00533       s_completionSources->append( resource->resourceName() );
00534       KABC::Resource::Iterator it;
00535       for ( it = resource->begin(); it != resource->end(); ++it )
00536         addContact( *it, weight, s_completionSources->size()-1 );
00537     }
00538   }
00539 
00540 #if 0 // now done as part of the normal contacts
00541   int weight = config.readNumEntry( "DistributionLists", 60 );
00542   KABC::DistributionListManager manager( addressBook );
00543   manager.load();
00544   const QStringList distLists = manager.listNames();
00545   QStringList::const_iterator listIt;
00546   int idx = addCompletionSource( i18n( "Distribution Lists" ) );
00547   for ( listIt = distLists.begin(); listIt != distLists.end(); ++listIt ) {
00548 
00549     //for KGlobalSettings::CompletionAuto
00550     addCompletionItem( (*listIt).simplifyWhiteSpace(), weight, idx );
00551 
00552     //for CompletionShell, CompletionPopup
00553     QStringList sl( (*listIt).simplifyWhiteSpace() );
00554     addCompletionItem( (*listIt).simplifyWhiteSpace(), weight, idx, &sl );
00555 
00556   }
00557 #endif
00558 
00559   QApplication::restoreOverrideCursor();
00560 
00561   if ( !m_addressBookConnected ) {
00562     connect( addressBook, SIGNAL( addressBookChanged( AddressBook* ) ), SLOT( loadContacts() ) );
00563     m_addressBookConnected = true;
00564   }
00565 }
00566 
00567 void AddresseeLineEdit::addContact( const KABC::Addressee& addr, int weight, int source )
00568 {
00569   if ( KPIM::DistributionList::isDistributionList( addr ) ) {
00570     //kdDebug(5300) << "AddresseeLineEdit::addContact() distribution list \"" << addr.formattedName() << "\" weight=" << weight << endl;
00571 
00572     //for CompletionAuto
00573     addCompletionItem( addr.formattedName(), weight, source );
00574 
00575     //for CompletionShell, CompletionPopup
00576     QStringList sl( addr.formattedName() );
00577     addCompletionItem( addr.formattedName(), weight, source, &sl );
00578 
00579     return;
00580   }
00581   //m_contactMap.insert( addr.realName(), addr );
00582   const QStringList emails = addr.emails();
00583   QStringList::ConstIterator it;
00584   const int prefEmailWeight = 1;     //increment weight by prefEmailWeight
00585   int isPrefEmail = prefEmailWeight; //first in list is preferredEmail
00586   for ( it = emails.begin(); it != emails.end(); ++it ) {
00587     //TODO: highlight preferredEmail
00588     const QString email( (*it) );
00589     const QString givenName = addr.givenName();
00590     const QString familyName= addr.familyName();
00591     const QString nickName  = addr.nickName();
00592     const QString domain    = email.mid( email.find( '@' ) + 1 );
00593     QString fullEmail = addr.fullEmail( email );
00594     //TODO: let user decide what fields to use in lookup, e.g. company, city, ...
00595 
00596     //for CompletionAuto
00597     if ( givenName.isEmpty() && familyName.isEmpty() ) {
00598       addCompletionItem( fullEmail, weight + isPrefEmail, source ); // use whatever is there
00599     } else {
00600       const QString byFirstName=  "\"" + givenName + " " + familyName + "\" <" + email + ">";
00601       const QString byLastName =  "\"" + familyName + ", " + givenName + "\" <" + email + ">";
00602       addCompletionItem( byFirstName, weight + isPrefEmail, source );
00603       addCompletionItem( byLastName, weight + isPrefEmail, source );
00604     }
00605 
00606     addCompletionItem( email, weight + isPrefEmail, source );
00607 
00608     if ( !nickName.isEmpty() ){
00609       const QString byNick     =  "\"" + nickName + "\" <" + email + ">";
00610       addCompletionItem( byNick, weight + isPrefEmail, source );
00611     }
00612 
00613     if ( !domain.isEmpty() ){
00614       const QString byDomain   =  "\"" + domain + " " + familyName + " " + givenName + "\" <" + email + ">";
00615       addCompletionItem( byDomain, weight + isPrefEmail, source );
00616     }
00617 
00618     //for CompletionShell, CompletionPopup
00619     QStringList keyWords;
00620     const QString realName  = addr.realName();
00621 
00622     if ( !givenName.isEmpty() && !familyName.isEmpty() ) {
00623       keyWords.append( givenName  + " "  + familyName );
00624       keyWords.append( familyName + " "  + givenName );
00625       keyWords.append( familyName + ", " + givenName);
00626     }else if ( !givenName.isEmpty() )
00627       keyWords.append( givenName );
00628     else if ( !familyName.isEmpty() )
00629       keyWords.append( familyName );
00630 
00631     if ( !nickName.isEmpty() )
00632       keyWords.append( nickName );
00633 
00634     if ( !realName.isEmpty() )
00635       keyWords.append( realName );
00636 
00637     if ( !domain.isEmpty() )
00638       keyWords.append( domain );
00639 
00640     keyWords.append( email );
00641 
00642     /* KMailCompletion does not have knowledge about identities, it stores emails and
00643      * keywords for each email. KMailCompletion::allMatches does a lookup on the
00644      * keywords and returns an ordered list of emails. In order to get the preferred
00645      * email before others for each identity we use this little trick.
00646      * We remove the <blank> in getAdjustedCompletionItems.
00647      */
00648     if ( isPrefEmail == prefEmailWeight )
00649       fullEmail.replace( " <", "  <" );
00650 
00651     addCompletionItem( fullEmail, weight + isPrefEmail, source, &keyWords );
00652     isPrefEmail = 0;
00653 
00654 #if 0
00655     int len = (*it).length();
00656     if ( len == 0 ) continue;
00657     if( '\0' == (*it)[len-1] )
00658       --len;
00659     const QString tmp = (*it).left( len );
00660     const QString fullEmail = addr.fullEmail( tmp );
00661     //kdDebug(5300) << "AddresseeLineEdit::addContact() \"" << fullEmail << "\" weight=" << weight << endl;
00662     addCompletionItem( fullEmail.simplifyWhiteSpace(), weight, source );
00663     // Try to guess the last name: if found, we add an extra
00664     // entry to the list to make sure completion works even
00665     // if the user starts by typing in the last name.
00666     QString name( addr.realName().simplifyWhiteSpace() );
00667     if( name.endsWith("\"") )
00668       name.truncate( name.length()-1 );
00669     if( name.startsWith("\"") )
00670       name = name.mid( 1 );
00671 
00672     // While we're here also add "email (full name)" for completion on the email
00673     if ( !name.isEmpty() )
00674       addCompletionItem( addr.preferredEmail() + " (" + name + ")", weight, source );
00675 
00676     bool bDone = false;
00677     int i = -1;
00678     while( ( i = name.findRev(' ') ) > 1 && !bDone ) {
00679       QString sLastName( name.mid( i+1 ) );
00680       if( ! sLastName.isEmpty() &&
00681             2 <= sLastName.length() &&   // last names must be at least 2 chars long
00682           ! sLastName.endsWith(".") ) { // last names must not end with a dot (like "Jr." or "Sr.")
00683         name.truncate( i );
00684         if( !name.isEmpty() ){
00685           sLastName.prepend( "\"" );
00686           sLastName.append( ", " + name + "\" <" );
00687         }
00688         QString sExtraEntry( sLastName );
00689         sExtraEntry.append( tmp.isEmpty() ? addr.preferredEmail() : tmp );
00690         sExtraEntry.append( ">" );
00691         //kdDebug(5300) << "AddresseeLineEdit::addContact() added extra \"" << sExtraEntry.simplifyWhiteSpace() << "\" weight=" << weight << endl;
00692         addCompletionItem( sExtraEntry.simplifyWhiteSpace(), weight, source );
00693         bDone = true;
00694       }
00695       if( !bDone ) {
00696         name.truncate( i );
00697         if( name.endsWith("\"") )
00698           name.truncate( name.length()-1 );
00699       }
00700     }
00701 #endif
00702   }
00703 }
00704 
00705 void AddresseeLineEdit::addCompletionItem( const QString& string, int weight, int completionItemSource, const QStringList * keyWords )
00706 {
00707   // Check if there is an exact match for item already, and use the max weight if so.
00708   // Since there's no way to get the information from KCompletion, we have to keep our own QMap
00709   CompletionItemsMap::iterator it = s_completionItemMap->find( string );
00710   if ( it != s_completionItemMap->end() ) {
00711     weight = QMAX( ( *it ).first, weight );
00712     ( *it ).first = weight;
00713   } else {
00714     s_completionItemMap->insert( string, qMakePair( weight, completionItemSource ) );
00715   }
00716   if ( keyWords == 0 )
00717     s_completion->addItem( string, weight );
00718   else
00719     s_completion->addItemWithKeys( string, weight, keyWords );
00720 }
00721 
00722 void AddresseeLineEdit::slotStartLDAPLookup()
00723 {
00724   if ( !s_LDAPSearch->isAvailable() ) {
00725     return;
00726   }
00727   if (  s_LDAPLineEdit != this )
00728     return;
00729 
00730   startLoadingLDAPEntries();
00731 }
00732 
00733 void AddresseeLineEdit::stopLDAPLookup()
00734 {
00735   s_LDAPSearch->cancelSearch();
00736   s_LDAPLineEdit = NULL;
00737 }
00738 
00739 void AddresseeLineEdit::startLoadingLDAPEntries()
00740 {
00741   QString s( *s_LDAPText );
00742   // TODO cache last?
00743   QString prevAddr;
00744   int n = s.findRev( ',' );
00745   if ( n >= 0 ) {
00746     prevAddr = s.left( n + 1 ) + ' ';
00747     s = s.mid( n + 1, 255 ).stripWhiteSpace();
00748   }
00749 
00750   if ( s.isEmpty() )
00751     return;
00752 
00753   //loadContacts(); // TODO reuse these?
00754   s_LDAPSearch->startSearch( s );
00755 }
00756 
00757 void AddresseeLineEdit::slotLDAPSearchData( const KPIM::LdapResultList& adrs )
00758 {
00759   if ( s_LDAPLineEdit != this )
00760     return;
00761 
00762   for ( KPIM::LdapResultList::ConstIterator it = adrs.begin(); it != adrs.end(); ++it ) {
00763     KABC::Addressee addr;
00764     addr.setNameFromString( (*it).name );
00765     addr.setEmails( (*it).email );
00766 
00767     addContact( addr, (*it).completionWeight, (*it ).clientNumber  );
00768   }
00769 
00770   if ( (hasFocus() || completionBox()->hasFocus() )
00771        && completionMode() != KGlobalSettings::CompletionNone
00772        && completionMode() != KGlobalSettings::CompletionShell) {
00773     setText( m_previousAddresses + m_searchString );
00774     doCompletion( m_lastSearchMode );
00775   }
00776 }
00777 
00778 // Workaround for bug in kdelibs < 3.4.beta2: KCompletionBox didn't resize
00779 // when calling setItems
00780 class KCompletionBoxHack : public KCompletionBox
00781 {
00782 public:
00783   KCompletionBoxHack() : KCompletionBox( 0 ) {}
00784   void sizeAndPosition() { KCompletionBox::sizeAndPosition(); }
00785 };
00786 
00787 void AddresseeLineEdit::setCompletedItems( const QStringList& items, bool autoSuggest )
00788 {
00789     KCompletionBox* completionBox = this->completionBox();
00790 
00791     if ( !items.isEmpty() &&
00792          !(items.count() == 1 && m_searchString == items.first()) )
00793     {
00794         QString oldCurrentText = completionBox->currentText();
00795         QListBoxItem *itemUnderMouse = completionBox->itemAt(
00796             completionBox->viewport()->mapFromGlobal(QCursor::pos()) );
00797         QString oldTextUnderMouse;
00798         QPoint oldPosOfItemUnderMouse;
00799         if ( itemUnderMouse ) {
00800             oldTextUnderMouse = itemUnderMouse->text();
00801             oldPosOfItemUnderMouse = completionBox->itemRect( itemUnderMouse ).topLeft();
00802         }
00803 
00804         completionBox->setItems( items );
00805         // Workaround for bug in kdelibs < 3.4.beta2: KCompletionBox didn't resize
00806         // when calling setItems
00807         KCompletionBoxHack* hack = static_cast<KCompletionBoxHack *>( completionBox );
00808         hack->sizeAndPosition();
00809 
00810         if ( !completionBox->isVisible() ) {
00811           if ( !m_searchString.isEmpty() )
00812             completionBox->setCancelledText( m_searchString );
00813           completionBox->popup();
00814           // we have to install the event filter after popup(), since that
00815           // calls show(), and that's where KCompletionBox installs its filter.
00816           // We want to be first, though, so do it now.
00817           if ( s_completion->order() == KCompletion::Weighted )
00818             qApp->installEventFilter( this );
00819         }
00820 
00821         // Try to re-select what was selected before, otherrwise use the first
00822         // item, if there is one
00823         QListBoxItem* item = 0;
00824         if ( oldCurrentText.isEmpty()
00825            || ( item = completionBox->findItem( oldCurrentText ) ) == 0 ) {
00826             item = completionBox->item( 1 );
00827         }
00828         if ( item )
00829         {
00830           if ( itemUnderMouse ) {
00831               QListBoxItem *newItemUnderMouse = completionBox->findItem( oldTextUnderMouse );
00832               // if the mouse was over an item, before, but now that's elsewhere,
00833               // move the cursor, so folks don't accidently click the wrong item
00834               if ( newItemUnderMouse ) {
00835                   QRect r = completionBox->itemRect( newItemUnderMouse );
00836                   QPoint target = r.topLeft();
00837                   if ( oldPosOfItemUnderMouse != target ) {
00838                       target.setX( target.x() + r.width()/2 );
00839                       QCursor::setPos( completionBox->viewport()->mapToGlobal(target) );
00840                   }
00841               }
00842           }
00843           completionBox->blockSignals( true );
00844           completionBox->setSelected( item, true );
00845           completionBox->setCurrentItem( item );
00846           completionBox->ensureCurrentVisible();
00847 
00848           completionBox->blockSignals( false );
00849         }
00850 
00851         if ( autoSuggest )
00852         {
00853             int index = items.first().find( m_searchString );
00854             QString newText = items.first().mid( index );
00855             setUserSelection(false);
00856             setCompletedText(newText,true);
00857         }
00858     }
00859     else
00860     {
00861         if ( completionBox && completionBox->isVisible() ) {
00862             completionBox->hide();
00863             completionBox->setItems( QStringList() );
00864         }
00865     }
00866 }
00867 
00868 QPopupMenu* AddresseeLineEdit::createPopupMenu()
00869 {
00870   QPopupMenu *menu = KLineEdit::createPopupMenu();
00871   if ( !menu )
00872     return 0;
00873 
00874   if ( m_useCompletion ){
00875     menu->setItemVisible( ShortAutoCompletion, false );
00876     menu->setItemVisible( PopupAutoCompletion, false );
00877     menu->insertItem( i18n( "Configure Completion Order..." ),
00878                       this, SLOT( slotEditCompletionOrder() ) );
00879   }
00880   return menu;
00881 }
00882 
00883 void AddresseeLineEdit::slotEditCompletionOrder()
00884 {
00885   init(); // for s_LDAPSearch
00886   CompletionOrderEditor editor( s_LDAPSearch, this );
00887   editor.exec();
00888 }
00889 
00890 void KPIM::AddresseeLineEdit::slotIMAPCompletionOrderChanged()
00891 {
00892   if ( m_useCompletion )
00893     s_addressesDirty = true;
00894 }
00895 
00896 void KPIM::AddresseeLineEdit::slotUserCancelled( const QString& cancelText )
00897 {
00898   if ( s_LDAPSearch && s_LDAPLineEdit == this )
00899     stopLDAPLookup();
00900   userCancelled( m_previousAddresses + cancelText ); // in KLineEdit
00901 }
00902 
00903 void AddresseeLineEdit::updateSearchString()
00904 {
00905   m_searchString = text();
00906 
00907   int n = -1;
00908   bool inQuote = false;
00909   for ( uint i = 0; i < m_searchString.length(); ++i ) {
00910     if ( m_searchString[ i ] == '"' )
00911       inQuote = !inQuote;
00912     if ( m_searchString[ i ] == '\\' && (i + 1) < m_searchString.length() && m_searchString[ i + 1 ] == '"' )
00913       ++i;
00914     if ( inQuote )
00915       continue;
00916     if ( m_searchString[ i ] == ',' || ( m_useSemiColonAsSeparator && m_searchString[ i ] == ';' ) )
00917       n = i;
00918   }
00919 
00920   if ( n >= 0 ) {
00921     ++n; // Go past the ","
00922 
00923     int len = m_searchString.length();
00924 
00925     // Increment past any whitespace...
00926     while ( n < len && m_searchString[ n ].isSpace() )
00927       ++n;
00928 
00929     m_previousAddresses = m_searchString.left( n );
00930     m_searchString = m_searchString.mid( n ).stripWhiteSpace();
00931   }
00932   else
00933   {
00934     m_previousAddresses = QString::null;
00935   }
00936 }
00937 
00938 void KPIM::AddresseeLineEdit::slotCompletion()
00939 {
00940   // Called by KLineEdit's keyPressEvent for CompletionModes Auto,Popup -> new text, update search string
00941   // not called for CompletionShell, this is been taken care of in AddresseeLineEdit::keyPressEvent
00942   updateSearchString();
00943   if ( completionBox() )
00944     completionBox()->setCancelledText( m_searchString );
00945   doCompletion( false );
00946 }
00947 
00948 // not cached, to make sure we get an up-to-date value when it changes
00949 KCompletion::CompOrder KPIM::AddresseeLineEdit::completionOrder()
00950 {
00951   KConfig config( "kpimcompletionorder" );
00952   config.setGroup( "General" );
00953   const QString order = config.readEntry( "CompletionOrder", "Weighted" );
00954 
00955   if ( order == "Weighted" )
00956     return KCompletion::Weighted;
00957   else
00958     return KCompletion::Sorted;
00959 }
00960 
00961 int KPIM::AddresseeLineEdit::addCompletionSource( const QString &source )
00962 {
00963   s_completionSources->append( source );
00964   return s_completionSources->size()-1;
00965 }
00966 
00967 bool KPIM::AddresseeLineEdit::eventFilter(QObject *obj, QEvent *e)
00968 {
00969   if ( obj == completionBox() ) {
00970     if ( e->type() == QEvent::MouseButtonPress
00971       || e->type() == QEvent::MouseMove
00972       || e->type() == QEvent::MouseButtonRelease ) {
00973       QMouseEvent* me = static_cast<QMouseEvent*>( e );
00974       // find list box item at the event position
00975       QListBoxItem *item = completionBox()->itemAt( me->pos() );
00976       if ( !item ) {
00977         // In the case of a mouse move outside of the box we don't want
00978         // the parent to fuzzy select a header by mistake.
00979         bool eat = e->type() == QEvent::MouseMove;
00980         return eat;
00981       }
00982       // avoid selection of headers on button press, or move or release while
00983       // a button is pressed
00984       if ( e->type() == QEvent::MouseButtonPress
00985           || me->state() & LeftButton || me->state() & MidButton
00986           || me->state() & RightButton ) {
00987         if ( itemIsHeader(item) ) {
00988           return true; // eat the event, we don't want anything to happen
00989         } else {
00990           // if we are not on one of the group heading, make sure the item
00991           // below or above is selected, not the heading, inadvertedly, due
00992           // to fuzzy auto-selection from QListBox
00993           completionBox()->setCurrentItem( item );
00994           completionBox()->setSelected( completionBox()->index( item ), true );
00995           if ( e->type() == QEvent::MouseMove )
00996             return true; // avoid fuzzy selection behavior
00997         }
00998       }
00999     }
01000   }
01001   if ( ( obj == this ) &&
01002      ( e->type() == QEvent::AccelOverride ) ) {
01003     QKeyEvent *ke = static_cast<QKeyEvent*>( e );
01004     if ( ke->key() == Key_Up || ke->key() == Key_Down || ke->key() == Key_Tab ) {
01005       ke->accept();
01006       return true;
01007     }
01008   }
01009   if ( ( obj == this ) &&
01010       ( e->type() == QEvent::KeyPress ) &&
01011       completionBox()->isVisible() ) {
01012     QKeyEvent *ke = static_cast<QKeyEvent*>( e );
01013     unsigned int currentIndex = completionBox()->currentItem();
01014     if ( ke->key() == Key_Up ) {
01015       //kdDebug() << "EVENTFILTER: Key_Up currentIndex=" << currentIndex << endl;
01016       // figure out if the item we would be moving to is one we want
01017       // to ignore. If so, go one further
01018       QListBoxItem *itemAbove = completionBox()->item( currentIndex - 1 );
01019       if ( itemAbove && itemIsHeader(itemAbove) ) {
01020         // there is a header above us, check if there is even further up
01021         // and if so go one up, so it'll be selected
01022         if ( currentIndex > 1 && completionBox()->item( currentIndex - 2 ) ) {
01023           //kdDebug() << "EVENTFILTER: Key_Up -> skipping " << currentIndex - 1 << endl;
01024           completionBox()->setCurrentItem( itemAbove->prev() );
01025           completionBox()->setSelected( currentIndex - 2, true );
01026         } else if ( currentIndex == 1 ) {
01027             // nothing to skip to, let's stay where we are, but make sure the
01028             // first header becomes visible, if we are the first real entry
01029             completionBox()->ensureVisible( 0, 0 );
01030             completionBox()->setSelected( currentIndex, true );
01031         }
01032         return true;
01033       }
01034     } else if ( ke->key() == Key_Down  ) {
01035       // same strategy for downwards
01036       //kdDebug() << "EVENTFILTER: Key_Down. currentIndex=" << currentIndex << endl;
01037       QListBoxItem *itemBelow = completionBox()->item( currentIndex + 1 );
01038       if ( itemBelow && itemIsHeader( itemBelow ) ) {
01039         if ( completionBox()->item( currentIndex + 2 ) ) {
01040           //kdDebug() << "EVENTFILTER: Key_Down -> skipping " << currentIndex+1 << endl;
01041           completionBox()->setCurrentItem( itemBelow->next() );
01042           completionBox()->setSelected( currentIndex + 2, true );
01043         } else {
01044           // nothing to skip to, let's stay where we are
01045           completionBox()->setSelected( currentIndex, true );
01046         }
01047         return true;
01048       }
01049       // special case of the last and only item in the list needing selection
01050       if ( !itemBelow && currentIndex == 1 ) {
01051         completionBox()->setSelected( currentIndex, true );
01052       }
01053       // special case of the initial selection, which is unfortunately a header.
01054       // Setting it to selected tricks KCompletionBox into not treating is special
01055       // and selecting making it current, instead of the one below.
01056       QListBoxItem *item = completionBox()->item( currentIndex );
01057       if ( item && itemIsHeader(item) ) {
01058         completionBox()->setSelected( currentIndex, true );
01059       }
01060     } else if ( ke->key() == Key_Tab || ke->key() == Key_Backtab ) {
01062       QListBoxItem *myHeader = 0;
01063       int i = currentIndex;
01064       while ( i>=0 ) {
01065         if ( itemIsHeader( completionBox()->item(i) ) ) {
01066           myHeader = completionBox()->item( i );
01067           break;
01068         }
01069         i--;
01070       }
01071       Q_ASSERT( myHeader ); // we should always be able to find a header
01072 
01073       // find the next header (searching backwards, for Key_Backtab
01074       QListBoxItem *nextHeader = 0;
01075       const int iterationstep = ke->key() == Key_Tab ?  1 : -1;
01076       // when iterating forward, start at the currentindex, when backwards,
01077       // one up from our header, or at the end
01078       uint j = ke->key() == Key_Tab ? currentIndex : i==0 ? completionBox()->count()-1 : (i-1) % completionBox()->count();
01079       while ( ( nextHeader = completionBox()->item( j ) ) && nextHeader != myHeader ) {
01080           if ( itemIsHeader(nextHeader) ) {
01081               break;
01082           }
01083           j = (j + iterationstep) % completionBox()->count();
01084       }
01085       if ( nextHeader && nextHeader != myHeader ) {
01086         QListBoxItem *item = completionBox()->item( j + 1 );
01087         if ( item && !itemIsHeader(item) ) {
01088           completionBox()->setSelected( j+1, true );
01089           completionBox()->setCurrentItem( item );
01090           completionBox()->ensureCurrentVisible();
01091         }
01092       }
01093       return true;
01094     }
01095   }
01096   return ClickLineEdit::eventFilter( obj, e );
01097 }
01098 
01099 const QStringList KPIM::AddresseeLineEdit::getAdjustedCompletionItems( bool fullSearch )
01100 {
01101   QStringList items = fullSearch ?
01102     s_completion->allMatches( m_searchString )
01103     : s_completion->substringCompletion( m_searchString );
01104 
01105   int lastSourceIndex = -1;
01106   unsigned int i = 0;
01107   QMap<int, QStringList> sections;
01108   QStringList sortedItems;
01109   for ( QStringList::Iterator it = items.begin(); it != items.end(); ++it, ++i ) {
01110     CompletionItemsMap::const_iterator cit = s_completionItemMap->find(*it);
01111     if ( cit == s_completionItemMap->end() )continue;
01112     int idx = (*cit).second;
01113     if ( s_completion->order() == KCompletion::Weighted ) {
01114       if ( lastSourceIndex == -1 || lastSourceIndex != idx ) {
01115         const QString sourceLabel(  (*s_completionSources)[idx] );
01116         if ( sections.find(idx) == sections.end() ) {
01117           items.insert( it, sourceLabel );
01118         }
01119         lastSourceIndex = idx;
01120       }
01121       (*it) = (*it).prepend( s_completionItemIndentString );
01122       // remove preferred email sort <blank> added in  addContact()
01123       (*it).replace( "  <", " <" );
01124     }
01125     sections[idx].append( *it );
01126 
01127     if ( s_completion->order() == KCompletion::Sorted ) {
01128       sortedItems.append( *it );
01129     }
01130   }
01131   if ( s_completion->order() == KCompletion::Weighted ) {
01132     for ( QMap<int, QStringList>::Iterator it( sections.begin() ), end( sections.end() ); it != end; ++it ) {
01133       sortedItems.append( (*s_completionSources)[it.key()] );
01134       for ( QStringList::Iterator sit( (*it).begin() ), send( (*it).end() ); sit != send; ++sit ) {
01135         sortedItems.append( *sit );
01136       }
01137     }
01138   } else {
01139     sortedItems.sort();
01140   }
01141   return sortedItems;
01142 }
01143 #include "addresseelineedit.moc"
KDE Logo
This file is part of the documentation for libkdepim Library Version 3.3.2.
Documentation copyright © 1996-2004 the KDE developers.
Generated on Fri Dec 21 14:23:16 2007 by doxygen 1.4.2 written by Dimitri van Heesch, © 1997-2003