libkdepim

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