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