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