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 pos = cursorPosition( );
00255 
00256   if ( hasSelectedText() ) {
00257     // Cut away the selection.
00258     start_sel = selectionStart();
00259     pos = start_sel;
00260     contents = contents.left( start_sel ) +
00261                contents.mid( start_sel + selectedText().length() );
00262   }
00263 
00264   int eot = contents.length();
00265   while ((eot > 0) && contents[ eot - 1 ].isSpace() ) eot--;
00266   if ( eot == 0 )
00267     contents = QString::null;
00268   else if ( pos >= eot ) {
00269     if ( contents[ eot - 1 ] == ',' )
00270       eot--;
00271     contents.truncate( eot );
00272     contents += ", ";
00273     pos = eot + 2;
00274   }
00275 
00276   contents = contents.left( pos ) + newText + contents.mid( pos );
00277   setText( contents );
00278   setEdited( true );
00279   setCursorPosition( pos + newText.length() );
00280 }
00281 
00282 void AddresseeLineEdit::setText( const QString & text )
00283 {
00284   ClickLineEdit::setText( text.stripWhiteSpace() );
00285 }
00286 
00287 void AddresseeLineEdit::paste()
00288 {
00289   if ( m_useCompletion )
00290     m_smartPaste = true;
00291 
00292   KLineEdit::paste();
00293   m_smartPaste = false;
00294 }
00295 
00296 void AddresseeLineEdit::mouseReleaseEvent( QMouseEvent *e )
00297 {
00298   // reimplemented from QLineEdit::mouseReleaseEvent()
00299   if ( m_useCompletion
00300        && QApplication::clipboard()->supportsSelection()
00301        && !isReadOnly()
00302        && e->button() == MidButton ) {
00303     m_smartPaste = true;
00304   }
00305 
00306   KLineEdit::mouseReleaseEvent( e );
00307   m_smartPaste = false;
00308 }
00309 
00310 void AddresseeLineEdit::dropEvent( QDropEvent *e )
00311 {
00312   KURL::List uriList;
00313   if ( !isReadOnly()
00314        && KURLDrag::canDecode(e) && KURLDrag::decode( e, uriList ) ) {
00315     QString contents = text();
00316     // remove trailing white space and comma
00317     int eot = contents.length();
00318     while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() )
00319       eot--;
00320     if ( eot == 0 )
00321       contents = QString::null;
00322     else if ( contents[ eot - 1 ] == ',' ) {
00323       eot--;
00324       contents.truncate( eot );
00325     }
00326     bool mailtoURL = false;
00327     // append the mailto URLs
00328     for ( KURL::List::Iterator it = uriList.begin();
00329           it != uriList.end(); ++it ) {
00330       if ( !contents.isEmpty() )
00331         contents.append( ", " );
00332       KURL u( *it );
00333       if ( u.protocol() == "mailto" ) {
00334         mailtoURL = true;
00335         contents.append( (*it).path() );
00336       }
00337     }
00338     if ( mailtoURL ) {
00339       setText( contents );
00340       setEdited( true );
00341       return;
00342     }
00343   }
00344 
00345   if ( m_useCompletion )
00346     m_smartPaste = true;
00347   QLineEdit::dropEvent( e );
00348   m_smartPaste = false;
00349 }
00350 
00351 void AddresseeLineEdit::cursorAtEnd()
00352 {
00353   setCursorPosition( text().length() );
00354 }
00355 
00356 void AddresseeLineEdit::enableCompletion( bool enable )
00357 {
00358   m_useCompletion = enable;
00359 }
00360 
00361 void AddresseeLineEdit::doCompletion( bool ctrlT )
00362 {
00363   m_lastSearchMode = ctrlT;
00364 
00365   KGlobalSettings::Completion  mode = completionMode();
00366 
00367   if ( mode == KGlobalSettings::CompletionNone  )
00368     return;
00369 
00370   if ( s_addressesDirty ) {
00371     loadContacts(); // read from local address book
00372     s_completion->setOrder( completionOrder() );
00373   }
00374 
00375   // cursor at end of string - or Ctrl+T pressed for substring completion?
00376   if ( ctrlT ) {
00377     const QStringList completions = getAdjustedCompletionItems( false );
00378 
00379     if ( completions.count() > 1 )
00380       ; //m_previousAddresses = prevAddr;
00381     else if ( completions.count() == 1 )
00382       setText( m_previousAddresses + completions.first().stripWhiteSpace() );
00383 
00384     setCompletedItems( completions, true ); // this makes sure the completion popup is closed if no matching items were found
00385 
00386     cursorAtEnd();
00387     setCompletionMode( mode ); //set back to previous mode
00388     return;
00389   }
00390 
00391 
00392   switch ( mode ) {
00393     case KGlobalSettings::CompletionPopupAuto:
00394     {
00395       if ( m_searchString.isEmpty() )
00396         break;
00397     }
00398 
00399     case KGlobalSettings::CompletionPopup:
00400     {
00401       const QStringList items = getAdjustedCompletionItems( true );
00402       setCompletedItems( items, false );
00403       break;
00404     }
00405 
00406     case KGlobalSettings::CompletionShell:
00407     {
00408       QString match = s_completion->makeCompletion( m_searchString );
00409       if ( !match.isNull() && match != m_searchString ) {
00410         setText( m_previousAddresses + match );
00411         setEdited( true );
00412         cursorAtEnd();
00413       }
00414       break;
00415     }
00416 
00417     case KGlobalSettings::CompletionMan: // Short-Auto in fact
00418     case KGlobalSettings::CompletionAuto:
00419     {
00420       //force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect
00421       setCompletionMode( completionMode() );
00422 
00423       if ( !m_searchString.isEmpty() ) {
00424 
00425         //if only our \" is left, remove it since user has not typed it either
00426         if ( m_searchExtended && m_searchString == "\"" ){
00427           m_searchExtended = false;
00428           m_searchString = QString::null;
00429           setText( m_previousAddresses );
00430           break;
00431         }
00432 
00433         QString match = s_completion->makeCompletion( m_searchString );
00434 
00435         if ( !match.isEmpty() ) {
00436           if ( match != m_searchString ) {
00437             QString adds = m_previousAddresses + match;
00438             setCompletedText( adds );
00439           }
00440         } else {
00441           if ( !m_searchString.startsWith( "\"" ) ) {
00442             //try with quoted text, if user has not type one already
00443             match = s_completion->makeCompletion( "\"" + m_searchString );
00444             if ( !match.isEmpty() && match != m_searchString ) {
00445               m_searchString = "\"" + m_searchString;
00446               m_searchExtended = true;
00447               setText( m_previousAddresses + m_searchString );
00448               setCompletedText( m_previousAddresses + match );
00449             }
00450           } else if ( m_searchExtended ) {
00451             //our added \" does not work anymore, remove it
00452             m_searchString = m_searchString.mid( 1 );
00453             m_searchExtended = false;
00454             setText( m_previousAddresses + m_searchString );
00455             //now try again
00456             match = s_completion->makeCompletion( m_searchString );
00457             if ( !match.isEmpty() && match != m_searchString ) {
00458               QString adds = m_previousAddresses + match;
00459               setCompletedText( adds );
00460             }
00461           }
00462         }
00463       }
00464       break;
00465     }
00466 
00467     case KGlobalSettings::CompletionNone:
00468     default: // fall through
00469       break;
00470   }
00471 }
00472 
00473 void AddresseeLineEdit::slotPopupCompletion( const QString& completion )
00474 {
00475   setText( m_previousAddresses + completion.stripWhiteSpace() );
00476   cursorAtEnd();
00477 //  slotMatched( m_previousAddresses + completion );
00478 }
00479 
00480 void AddresseeLineEdit::slotReturnPressed( const QString& item )
00481 {
00482   Q_UNUSED( item );
00483   QListBoxItem* i = completionBox()->selectedItem();
00484   if ( i != 0 )
00485     slotPopupCompletion( i->text() );
00486 }
00487 
00488 void AddresseeLineEdit::loadContacts()
00489 {
00490   s_completion->clear();
00491   s_completionItemMap->clear();
00492   s_addressesDirty = false;
00493   //m_contactMap.clear();
00494 
00495   QApplication::setOverrideCursor( KCursor::waitCursor() ); // loading might take a while
00496 
00497   KConfig config( "kpimcompletionorder" ); // The weights for non-imap kabc resources is there.
00498   config.setGroup( "CompletionWeights" );
00499 
00500   KABC::AddressBook *addressBook = KABC::StdAddressBook::self( true );
00501   // Can't just use the addressbook's iterator, we need to know which subresource
00502   // is behind which contact.
00503   QPtrList<KABC::Resource> resources( addressBook->resources() );
00504   for( QPtrListIterator<KABC::Resource> resit( resources ); *resit; ++resit ) {
00505     KABC::Resource* resource = *resit;
00506     KPIM::ResourceABC* resabc = dynamic_cast<ResourceABC *>( resource );
00507     if ( resabc ) { // IMAP KABC resource; need to associate each contact with the subresource
00508       const QMap<QString, QString> uidToResourceMap = resabc->uidToResourceMap();
00509       KABC::Resource::Iterator it;
00510       for ( it = resource->begin(); it != resource->end(); ++it ) {
00511         QString uid = (*it).uid();
00512         QMap<QString, QString>::const_iterator wit = uidToResourceMap.find( uid );
00513         const QString subresourceLabel = resabc->subresourceLabel( *wit );
00514         int idx = s_completionSources->findIndex( subresourceLabel );
00515         if ( idx == -1 ) {
00516           s_completionSources->append( subresourceLabel );
00517           idx = s_completionSources->size() -1;
00518         }
00519         int weight = ( wit != uidToResourceMap.end() ) ? resabc->subresourceCompletionWeight( *wit ) : 80;
00520         //kdDebug(5300) << (*it).fullEmail() << " subres=" << *wit << " weight=" << weight << endl;
00521         addContact( *it, weight, idx );
00522       }
00523     } else { // KABC non-imap resource
00524       int weight = config.readNumEntry( resource->identifier(), 60 );
00525       s_completionSources->append( resource->resourceName() );
00526       KABC::Resource::Iterator it;
00527       for ( it = resource->begin(); it != resource->end(); ++it )
00528         addContact( *it, weight, s_completionSources->size()-1 );
00529     }
00530   }
00531 
00532 #ifndef KDEPIM_NEW_DISTRLISTS // new distr lists are normal contact, already done above
00533   int weight = config.readNumEntry( "DistributionLists", 60 );
00534   KABC::DistributionListManager manager( addressBook );
00535   manager.load();
00536   const QStringList distLists = manager.listNames();
00537   QStringList::const_iterator listIt;
00538   int idx = addCompletionSource( i18n( "Distribution Lists" ) );
00539   for ( listIt = distLists.begin(); listIt != distLists.end(); ++listIt ) {
00540 
00541     //for KGlobalSettings::CompletionAuto
00542     addCompletionItem( (*listIt).simplifyWhiteSpace(), weight, idx );
00543 
00544     //for CompletionShell, CompletionPopup
00545     QStringList sl( (*listIt).simplifyWhiteSpace() );
00546     addCompletionItem( (*listIt).simplifyWhiteSpace(), weight, idx, &sl );
00547 
00548   }
00549 #endif
00550 
00551   QApplication::restoreOverrideCursor();
00552 
00553   if ( !m_addressBookConnected ) {
00554     connect( addressBook, SIGNAL( addressBookChanged( AddressBook* ) ), SLOT( loadContacts() ) );
00555     m_addressBookConnected = true;
00556   }
00557 }
00558 
00559 void AddresseeLineEdit::addContact( const KABC::Addressee& addr, int weight, int source )
00560 {
00561 #ifdef KDEPIM_NEW_DISTRLISTS
00562   if ( KPIM::DistributionList::isDistributionList( addr ) ) {
00563     //kdDebug(5300) << "AddresseeLineEdit::addContact() distribution list \"" << addr.formattedName() << "\" weight=" << weight << endl;
00564 
00565     //for CompletionAuto
00566     addCompletionItem( addr.formattedName(), weight, source );
00567 
00568     //for CompletionShell, CompletionPopup
00569     QStringList sl( addr.formattedName() );
00570     addCompletionItem( addr.formattedName(), weight, source, &sl );
00571 
00572     return;
00573   }
00574 #endif
00575   //m_contactMap.insert( addr.realName(), addr );
00576   const QStringList emails = addr.emails();
00577   QStringList::ConstIterator it;
00578   for ( it = emails.begin(); it != emails.end(); ++it ) {
00579     //TODO: highlight preferredEmail
00580     const QString email( (*it) );
00581     const QString givenName = addr.givenName();
00582     const QString familyName= addr.familyName();
00583     const QString nickName  = addr.nickName();
00584     const QString fullEmail = addr.fullEmail( email );
00585     const QString domain    = email.mid( email.find( '@' ) + 1 );
00586     //TODO: let user decide what fields to use in lookup, e.g. company, city, ...
00587 
00588     //for CompletionAuto
00589     if ( givenName.isEmpty() && familyName.isEmpty() ) {
00590       addCompletionItem( fullEmail, weight, source ); // use whatever is there
00591     } else {
00592       const QString byFirstName=  "\"" + givenName + " " + familyName + "\" <" + email + ">";
00593       const QString byLastName =  "\"" + familyName + ", " + givenName + "\" <" + email + ">";
00594       addCompletionItem( byFirstName, weight, source );
00595       addCompletionItem( byLastName, weight, source );
00596     }
00597 
00598     addCompletionItem( email, weight, source );
00599 
00600     if ( !nickName.isEmpty() ){
00601       const QString byNick     =  "\"" + nickName + "\" <" + email + ">";
00602       addCompletionItem( byNick, weight, source );
00603     }
00604 
00605     if ( !domain.isEmpty() ){
00606       const QString byDomain   =  "\"" + domain + " " + familyName + " " + givenName + "\" <" + email + ">";
00607       addCompletionItem( byDomain, weight, source );
00608     }
00609 
00610     //for CompletionShell, CompletionPopup
00611     QStringList keyWords;
00612     const QString realName  = addr.realName();
00613 
00614     if ( !givenName.isEmpty() && !familyName.isEmpty() ) {
00615       keyWords.append( givenName  + " "  + familyName );
00616       keyWords.append( familyName + " "  + givenName );
00617       keyWords.append( familyName + ", " + givenName);
00618     }else if ( !givenName.isEmpty() )
00619       keyWords.append( givenName );
00620     else if ( !familyName.isEmpty() )
00621       keyWords.append( familyName );
00622 
00623     if ( !nickName.isEmpty() )
00624       keyWords.append( nickName );
00625 
00626     if ( !realName.isEmpty() )
00627       keyWords.append( realName );
00628 
00629     if ( !domain.isEmpty() )
00630       keyWords.append( domain );
00631 
00632     keyWords.append( email );
00633 
00634     addCompletionItem( fullEmail, weight, source, &keyWords );
00635 
00636 #if 0
00637     int len = (*it).length();
00638     if ( len == 0 ) continue;
00639     if( '\0' == (*it)[len-1] )
00640       --len;
00641     const QString tmp = (*it).left( len );
00642     const QString fullEmail = addr.fullEmail( tmp );
00643     //kdDebug(5300) << "AddresseeLineEdit::addContact() \"" << fullEmail << "\" weight=" << weight << endl;
00644     addCompletionItem( fullEmail.simplifyWhiteSpace(), weight, source );
00645     // Try to guess the last name: if found, we add an extra
00646     // entry to the list to make sure completion works even
00647     // if the user starts by typing in the last name.
00648     QString name( addr.realName().simplifyWhiteSpace() );
00649     if( name.endsWith("\"") )
00650       name.truncate( name.length()-1 );
00651     if( name.startsWith("\"") )
00652       name = name.mid( 1 );
00653 
00654     // While we're here also add "email (full name)" for completion on the email
00655     if ( !name.isEmpty() )
00656       addCompletionItem( addr.preferredEmail() + " (" + name + ")", weight, source );
00657 
00658     bool bDone = false;
00659     int i = -1;
00660     while( ( i = name.findRev(' ') ) > 1 && !bDone ) {
00661       QString sLastName( name.mid( i+1 ) );
00662       if( ! sLastName.isEmpty() &&
00663             2 <= sLastName.length() &&   // last names must be at least 2 chars long
00664           ! sLastName.endsWith(".") ) { // last names must not end with a dot (like "Jr." or "Sr.")
00665         name.truncate( i );
00666         if( !name.isEmpty() ){
00667           sLastName.prepend( "\"" );
00668           sLastName.append( ", " + name + "\" <" );
00669         }
00670         QString sExtraEntry( sLastName );
00671         sExtraEntry.append( tmp.isEmpty() ? addr.preferredEmail() : tmp );
00672         sExtraEntry.append( ">" );
00673         //kdDebug(5300) << "AddresseeLineEdit::addContact() added extra \"" << sExtraEntry.simplifyWhiteSpace() << "\" weight=" << weight << endl;
00674         addCompletionItem( sExtraEntry.simplifyWhiteSpace(), weight, source );
00675         bDone = true;
00676       }
00677       if( !bDone ) {
00678         name.truncate( i );
00679         if( name.endsWith("\"") )
00680           name.truncate( name.length()-1 );
00681       }
00682     }
00683 #endif
00684   }
00685 }
00686 
00687 void AddresseeLineEdit::addCompletionItem( const QString& string, int weight, int completionItemSource, const QStringList * keyWords )
00688 {
00689   // Check if there is an exact match for item already, and use the max weight if so.
00690   // Since there's no way to get the information from KCompletion, we have to keep our own QMap
00691   CompletionItemsMap::iterator it = s_completionItemMap->find( string );
00692   if ( it != s_completionItemMap->end() ) {
00693     weight = QMAX( ( *it ).first, weight );
00694     ( *it ).first = weight;
00695   } else {
00696     s_completionItemMap->insert( string, qMakePair( weight, completionItemSource ) );
00697   }
00698   if ( keyWords == 0 )
00699     s_completion->addItem( string, weight );
00700   else
00701     s_completion->addItemWithKeys( string, weight, keyWords );
00702 }
00703 
00704 void AddresseeLineEdit::slotStartLDAPLookup()
00705 {
00706   if ( !s_LDAPSearch->isAvailable() ) {
00707     return;
00708   }
00709   if (  s_LDAPLineEdit != this )
00710     return;
00711 
00712   startLoadingLDAPEntries();
00713 }
00714 
00715 void AddresseeLineEdit::stopLDAPLookup()
00716 {
00717   s_LDAPSearch->cancelSearch();
00718   s_LDAPLineEdit = NULL;
00719 }
00720 
00721 void AddresseeLineEdit::startLoadingLDAPEntries()
00722 {
00723   QString s( *s_LDAPText );
00724   // TODO cache last?
00725   QString prevAddr;
00726   int n = s.findRev( ',' );
00727   if ( n >= 0 ) {
00728     prevAddr = s.left( n + 1 ) + ' ';
00729     s = s.mid( n + 1, 255 ).stripWhiteSpace();
00730   }
00731 
00732   if ( s.isEmpty() )
00733     return;
00734 
00735   loadContacts(); // TODO reuse these?
00736   s_LDAPSearch->startSearch( s );
00737 }
00738 
00739 void AddresseeLineEdit::slotLDAPSearchData( const KPIM::LdapResultList& adrs )
00740 {
00741   if ( s_LDAPLineEdit != this )
00742     return;
00743 
00744   for ( KPIM::LdapResultList::ConstIterator it = adrs.begin(); it != adrs.end(); ++it ) {
00745     KABC::Addressee addr;
00746     addr.setNameFromString( (*it).name );
00747     addr.setEmails( (*it).email );
00748 
00749     addContact( addr, (*it).completionWeight, (*it ).clientNumber  );
00750   }
00751 
00752   if ( (hasFocus() || completionBox()->hasFocus() )
00753        && completionMode() != KGlobalSettings::CompletionNone
00754        && completionMode() != KGlobalSettings::CompletionShell) {
00755     setText( m_previousAddresses + m_searchString );
00756     doCompletion( m_lastSearchMode );
00757   }
00758 }
00759 
00760 void AddresseeLineEdit::setCompletedItems( const QStringList& items, bool autoSuggest )
00761 {
00762     KCompletionBox* completionBox = this->completionBox();
00763 
00764     if ( !items.isEmpty() &&
00765          !(items.count() == 1 && m_searchString == items.first()) )
00766     {
00767         completionBox->setItems( items );
00768 
00769         if ( !completionBox->isVisible() ) {
00770           if ( !m_searchString.isEmpty() )
00771             completionBox->setCancelledText( m_searchString );
00772           completionBox->popup();
00773           // we have to install the event filter after popup(), since that
00774           // calls show(), and that's where KCompletionBox installs its filter.
00775           // We want to be first, though, so do it now.
00776           if ( s_completion->order() == KCompletion::Weighted )
00777             qApp->installEventFilter( this );
00778         }
00779 
00780         QListBoxItem* item = completionBox->item( 1 );
00781         if ( item )
00782         {
00783           completionBox->blockSignals( true );
00784           completionBox->setSelected( item, true );
00785           completionBox->blockSignals( false );
00786         }
00787 
00788         if ( autoSuggest )
00789         {
00790             int index = items.first().find( m_searchString );
00791             QString newText = items.first().mid( index );
00792             setUserSelection(false);
00793             setCompletedText(newText,true);
00794         }
00795     }
00796     else
00797     {
00798         if ( completionBox && completionBox->isVisible() ) {
00799             completionBox->hide();
00800             completionBox->setItems( QStringList() );
00801         }
00802     }
00803 }
00804 
00805 QPopupMenu* AddresseeLineEdit::createPopupMenu()
00806 {
00807   QPopupMenu *menu = KLineEdit::createPopupMenu();
00808   if ( !menu )
00809     return 0;
00810 
00811   if ( m_useCompletion ){
00812     menu->setItemVisible( ShortAutoCompletion, false );
00813     menu->setItemVisible( PopupAutoCompletion, false );
00814     menu->insertItem( i18n( "Configure Completion Order..." ),
00815                       this, SLOT( slotEditCompletionOrder() ) );
00816   }
00817   return menu;
00818 }
00819 
00820 void AddresseeLineEdit::slotEditCompletionOrder()
00821 {
00822   init(); // for s_LDAPSearch
00823   CompletionOrderEditor editor( s_LDAPSearch, this );
00824   editor.exec();
00825 }
00826 
00827 void KPIM::AddresseeLineEdit::slotIMAPCompletionOrderChanged()
00828 {
00829   if ( m_useCompletion )
00830     s_addressesDirty = true;
00831 }
00832 
00833 void KPIM::AddresseeLineEdit::slotUserCancelled( const QString& cancelText )
00834 {
00835   if ( s_LDAPSearch && s_LDAPLineEdit == this )
00836     stopLDAPLookup();
00837   userCancelled( m_previousAddresses + cancelText ); // in KLineEdit
00838 }
00839 
00840 void AddresseeLineEdit::updateSearchString()
00841 {
00842   m_searchString = text();
00843   int n = m_searchString.findRev(',');
00844   if ( n >= 0 ) {
00845     ++n; // Go past the ","
00846 
00847     int len = m_searchString.length();
00848 
00849     // Increment past any whitespace...
00850     while ( n < len && m_searchString[ n ].isSpace() )
00851       ++n;
00852 
00853     m_previousAddresses = m_searchString.left( n );
00854     m_searchString = m_searchString.mid( n ).stripWhiteSpace();
00855   }
00856   else
00857   {
00858     m_previousAddresses = QString::null;
00859   }
00860 }
00861 
00862 void KPIM::AddresseeLineEdit::slotCompletion()
00863 {
00864   // Called by KLineEdit's keyPressEvent for CompletionModes Auto,Popup -> new text, update search string
00865   // not called for CompletionShell, this is been taken care of in AddresseeLineEdit::keyPressEvent
00866   updateSearchString();
00867   if ( completionBox() )
00868     completionBox()->setCancelledText( m_searchString );
00869   doCompletion( false );
00870 }
00871 
00872 // not cached, to make sure we get an up-to-date value when it changes
00873 KCompletion::CompOrder KPIM::AddresseeLineEdit::completionOrder()
00874 {
00875   KConfig config( "kpimcompletionorder" );
00876   config.setGroup( "General" );
00877   const QString order = config.readEntry( "CompletionOrder", "Weighted" );
00878 
00879   if ( order == "Weighted" )
00880     return KCompletion::Weighted;
00881   else
00882     return KCompletion::Sorted;
00883 }
00884 
00885 int KPIM::AddresseeLineEdit::addCompletionSource( const QString &source )
00886 {
00887   s_completionSources->append( source );
00888   return s_completionSources->size()-1;
00889 }
00890 
00891 bool KPIM::AddresseeLineEdit::eventFilter(QObject *obj, QEvent *e)
00892 {
00893   if ( obj == completionBox() ) {
00894     if ( e->type() == QEvent::MouseButtonPress
00895       || e->type() == QEvent::MouseMove
00896       || e->type() == QEvent::MouseButtonRelease ) {
00897       QMouseEvent* me = static_cast<QMouseEvent*>( e );
00898       // find list box item at the event position
00899       QListBoxItem *item = completionBox()->itemAt( me->pos() );
00900       if ( !item ) {
00901         // In the case of a mouse move outside of the box we don't want
00902         // the parent to fuzzy select a header by mistake.
00903         bool eat = e->type() == QEvent::MouseMove;
00904         return eat;
00905       }
00906       // avoid selection of headers on button press, or move or release while
00907       // a button is pressed
00908       if ( e->type() == QEvent::MouseButtonPress
00909           || me->state() & LeftButton || me->state() & MidButton
00910           || me->state() & RightButton ) {
00911         if ( !item->text().startsWith( s_completionItemIndentString ) ) {
00912           return true; // eat the event, we don't want anything to happen
00913         } else {
00914           // if we are not on one of the group heading, make sure the item
00915           // below or above is selected, not the heading, inadvertedly, due
00916           // to fuzzy auto-selection from QListBox
00917           completionBox()->setCurrentItem( item );
00918           completionBox()->setSelected( completionBox()->index( item ), true );
00919           if ( e->type() == QEvent::MouseMove )
00920             return true; // avoid fuzzy selection behavior
00921         }
00922       }
00923     }
00924   }
00925   if ( ( obj == this ) &&
00926      ( e->type() == QEvent::AccelOverride ) ) {
00927     QKeyEvent *ke = static_cast<QKeyEvent*>( e );
00928     if ( ke->key() == Key_Up || ke->key() == Key_Down || ke->key() == Key_Tab ) {
00929       ke->accept();
00930       return true;
00931     }
00932   }
00933   if ( ( obj == this ) &&
00934       ( e->type() == QEvent::KeyPress ) &&
00935       completionBox()->isVisible() ) {
00936     QKeyEvent *ke = static_cast<QKeyEvent*>( e );
00937     unsigned int currentIndex = completionBox()->currentItem();
00938     if ( ke->key() == Key_Up || ke->key() == Key_Backtab ) {
00939       //kdDebug() << "EVENTFILTER: Key_Up currentIndex=" << currentIndex << endl;
00940       // figure out if the item we would be moving to is one we want
00941       // to ignore. If so, go one further
00942       QListBoxItem *itemAbove = completionBox()->item( currentIndex - 1 );
00943       if ( itemAbove && !itemAbove->text().startsWith( s_completionItemIndentString ) ) {
00944         // there is a header above us, check if there is even further up
00945         // and if so go one up, so it'll be selected
00946         if ( currentIndex > 1 && completionBox()->item( currentIndex - 2 ) ) {
00947           //kdDebug() << "EVENTFILTER: Key_Up -> skipping " << currentIndex - 1 << endl;
00948           completionBox()->setCurrentItem( itemAbove->prev() );
00949           completionBox()->setSelected( currentIndex - 2, true );
00950         } else if ( currentIndex == 1 ) {
00951             // nothing to skip to, let's stay where we are, but make sure the
00952             // first header becomes visible, if we are the first real entry
00953             completionBox()->ensureVisible( 0, 0 );
00954             completionBox()->setSelected( currentIndex, true );
00955         }
00956         return true;
00957       }
00958     } else if ( ke->key() == Key_Down || ke->key() == Key_Tab ) {
00959       // same strategy for downwards
00960       //kdDebug() << "EVENTFILTER: Key_Down. currentIndex=" << currentIndex << endl;
00961       QListBoxItem *itemBelow = completionBox()->item( currentIndex + 1 );
00962       if ( itemBelow && !itemBelow->text().startsWith( s_completionItemIndentString ) ) {
00963         if ( completionBox()->item( currentIndex + 2 ) ) {
00964           //kdDebug() << "EVENTFILTER: Key_Down -> skipping " << currentIndex+1 << endl;
00965           completionBox()->setCurrentItem( itemBelow->next() );
00966           completionBox()->setSelected( currentIndex + 2, true );
00967         } else {
00968           // nothing to skip to, let's stay where we are
00969           completionBox()->setSelected( currentIndex, true );
00970         }
00971         return true;
00972       }
00973       // special case of the last and only item in the list needing selection
00974       if ( !itemBelow && currentIndex == 1 ) {
00975         completionBox()->setSelected( currentIndex, true );
00976       }
00977       // special case of the initial selection, which is unfortunately a header.
00978       // Setting it to selected tricks KCompletionBox into not treating is special
00979       // and selecting making it current, instead of the one below.
00980       QListBoxItem *item = completionBox()->item( currentIndex );
00981       if ( item && !item->text().startsWith( s_completionItemIndentString )  ) {
00982         completionBox()->setSelected( currentIndex, true );
00983       }
00984     }
00985   }
00986   return ClickLineEdit::eventFilter( obj, e );
00987 }
00988 
00989 const QStringList KPIM::AddresseeLineEdit::getAdjustedCompletionItems( bool fullSearch )
00990 {
00991   QStringList items = fullSearch ?
00992     s_completion->allMatches( m_searchString )
00993     : s_completion->substringCompletion( m_searchString );
00994 
00995   int lastSourceIndex = -1;
00996   unsigned int i = 0;
00997   QMap<int, QStringList> sections;
00998   QStringList sortedItems;
00999   for ( QStringList::Iterator it = items.begin(); it != items.end(); ++it, ++i ) {
01000     CompletionItemsMap::const_iterator cit = s_completionItemMap->find(*it);
01001     if ( cit == s_completionItemMap->end() )continue;
01002     int idx = (*cit).second;
01003     if ( s_completion->order() == KCompletion::Weighted ) {
01004       if ( lastSourceIndex == -1 || lastSourceIndex != idx ) {
01005         const QString sourceLabel(  (*s_completionSources)[idx] );
01006         if ( sections.find(idx) == sections.end() ) {
01007           items.insert( it, sourceLabel );
01008         }
01009         lastSourceIndex = idx;
01010       }
01011       (*it) = (*it).prepend( s_completionItemIndentString );
01012     }
01013     sections[idx].append( *it );
01014 
01015     if ( s_completion->order() == KCompletion::Sorted ) {
01016       sortedItems.append( *it );
01017     }
01018   }
01019   if ( s_completion->order() == KCompletion::Weighted ) {
01020     for ( QMap<int, QStringList>::Iterator it( sections.begin() ), end( sections.end() ); it != end; ++it ) {
01021       sortedItems.append( (*s_completionSources)[it.key()] );
01022       for ( QStringList::Iterator sit( (*it).begin() ), send( (*it).end() ); sit != send; ++sit ) {
01023         sortedItems.append( *sit );
01024       }
01025     }
01026   } else {
01027     sortedItems.sort();
01028   }
01029   return sortedItems;
01030 }
01031 #include "addresseelineedit.moc"
KDE Home | KDE Accessibility Home | Description of Access Keys