/* This file is part of libtdepim. Copyright (c) 2002 Helge Deller <deller@gmx.de> 2002 Lubos Lunak <llunak@suse.cz> 2001,2003 Carsten Pfeiffer <pfeiffer@kde.org> 2001 Waldo Bastian <bastian@kde.org> 2004 Daniel Molkentin <danimo@klaralvdalens-datakonsult.se> 2004 Karl-Heinz Zimmer <khz@klaralvdalens-datakonsult.se> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "addresseelineedit.h" #include "resourceabc.h" #include "completionordereditor.h" #include "ldapclient.h" #include <config.h> #ifdef TDEPIM_NEW_DISTRLISTS #include "distributionlist.h" #else #include <kabc/distributionlist.h> #endif #include <kabc/stdaddressbook.h> #include <kabc/resource.h> #include <libemailfunctions/email.h> #include <kcompletionbox.h> #include <kcursor.h> #include <kdebug.h> #include <kstandarddirs.h> #include <kstaticdeleter.h> #include <kstdaccel.h> #include <kurldrag.h> #include <klocale.h> #include <tqpopupmenu.h> #include <tqapplication.h> #include <tqobject.h> #include <tqptrlist.h> #include <tqregexp.h> #include <tqevent.h> #include <tqdragobject.h> #include <tqclipboard.h> using namespace KPIM; KMailCompletion * AddresseeLineEdit::s_completion = 0L; KPIM::CompletionItemsMap* AddresseeLineEdit::s_completionItemMap = 0L; TQStringList* AddresseeLineEdit::s_completionSources = 0L; bool AddresseeLineEdit::s_addressesDirty = false; TQTimer* AddresseeLineEdit::s_LDAPTimer = 0L; KPIM::LdapSearch* AddresseeLineEdit::s_LDAPSearch = 0L; TQString* AddresseeLineEdit::s_LDAPText = 0L; AddresseeLineEdit* AddresseeLineEdit::s_LDAPLineEdit = 0L; // The weights associated with the completion sources in s_completionSources. // Both are maintained by addCompletionSource(), don't attempt to modifiy those yourself. TQMap<TQString,int>* s_completionSourceWeights = 0; // maps LDAP client indices to completion source indices // the assumption that they are always the first n indices in s_completion // does not hold when clients are added later on TQMap<int, int>* AddresseeLineEdit::s_ldapClientToCompletionSourceMap = 0; static KStaticDeleter<KMailCompletion> completionDeleter; static KStaticDeleter<KPIM::CompletionItemsMap> completionItemsDeleter; static KStaticDeleter<TQTimer> ldapTimerDeleter; static KStaticDeleter<KPIM::LdapSearch> ldapSearchDeleter; static KStaticDeleter<TQString> ldapTextDeleter; static KStaticDeleter<TQStringList> completionSourcesDeleter; static KStaticDeleter<TQMap<TQString,int> > completionSourceWeightsDeleter; static KStaticDeleter<TQMap<int, int> > ldapClientToCompletionSourceMapDeleter; // needs to be unique, but the actual name doesn't matter much static TQCString newLineEditDCOPObjectName() { static int s_count = 0; TQCString name( "KPIM::AddresseeLineEdit" ); if ( s_count++ ) { name += '-'; name += TQCString().setNum( s_count ); } return name; } static const TQString s_completionItemIndentString = " "; static bool itemIsHeader( const TQListBoxItem* item ) { return item && !item->text().startsWith( s_completionItemIndentString ); } AddresseeLineEdit::AddresseeLineEdit( TQWidget* parent, bool useCompletion, const char *name ) : ClickLineEdit( parent, TQString(), name ), DCOPObject( newLineEditDCOPObjectName() ), m_useSemiColonAsSeparator( false ), m_allowDistLists( true ) { m_useCompletion = useCompletion; m_completionInitialized = false; m_smartPaste = false; m_addressBookConnected = false; m_searchExtended = false; init(); if ( m_useCompletion ) s_addressesDirty = true; } void AddresseeLineEdit::updateLDAPWeights() { /* Add completion sources for all ldap server, 0 to n. Added first so * that they map to the ldapclient::clientNumber() */ s_LDAPSearch->updateCompletionWeights(); TQValueList< LdapClient* > clients = s_LDAPSearch->clients(); int clientIndex = 0; for ( TQValueList<LdapClient*>::iterator it = clients.begin(); it != clients.end(); ++it, ++clientIndex ) { const int sourceIndex = addCompletionSource( "LDAP server: " + (*it)->server().host(), (*it)->completionWeight() ); s_ldapClientToCompletionSourceMap->insert( clientIndex, sourceIndex ); } } void AddresseeLineEdit::init() { if ( !s_completion ) { completionDeleter.setObject( s_completion, new KMailCompletion() ); s_completion->setOrder( completionOrder() ); s_completion->setIgnoreCase( true ); completionItemsDeleter.setObject( s_completionItemMap, new KPIM::CompletionItemsMap() ); completionSourcesDeleter.setObject( s_completionSources, new TQStringList() ); completionSourceWeightsDeleter.setObject( s_completionSourceWeights, new TQMap<TQString,int> ); ldapClientToCompletionSourceMapDeleter.setObject( s_ldapClientToCompletionSourceMap, new TQMap<int,int> ); } // connect( s_completion, TQT_SIGNAL( match( const TQString& ) ), // this, TQT_SLOT( slotMatched( const TQString& ) ) ); if ( m_useCompletion ) { if ( !s_LDAPTimer ) { ldapTimerDeleter.setObject( s_LDAPTimer, new TQTimer( 0, "ldapTimerDeleter" ) ); ldapSearchDeleter.setObject( s_LDAPSearch, new KPIM::LdapSearch ); ldapTextDeleter.setObject( s_LDAPText, new TQString ); } updateLDAPWeights(); if ( !m_completionInitialized ) { setCompletionObject( s_completion, false ); connect( this, TQT_SIGNAL( completion( const TQString& ) ), this, TQT_SLOT( slotCompletion() ) ); connect( this, TQT_SIGNAL( returnPressed( const TQString& ) ), this, TQT_SLOT( slotReturnPressed( const TQString& ) ) ); KCompletionBox *box = completionBox(); connect( box, TQT_SIGNAL( highlighted( const TQString& ) ), this, TQT_SLOT( slotPopupCompletion( const TQString& ) ) ); connect( box, TQT_SIGNAL( userCancelled( const TQString& ) ), TQT_SLOT( slotUserCancelled( const TQString& ) ) ); // The emitter is always called KPIM::IMAPCompletionOrder by contract if ( !connectDCOPSignal( 0, "KPIM::IMAPCompletionOrder", "orderChanged()", "slotIMAPCompletionOrderChanged()", false ) ) kdError() << "AddresseeLineEdit: connection to orderChanged() failed" << endl; connect( s_LDAPTimer, TQT_SIGNAL( timeout() ), TQT_SLOT( slotStartLDAPLookup() ) ); connect( s_LDAPSearch, TQT_SIGNAL( searchData( const KPIM::LdapResultList& ) ), TQT_SLOT( slotLDAPSearchData( const KPIM::LdapResultList& ) ) ); m_completionInitialized = true; } } } AddresseeLineEdit::~AddresseeLineEdit() { if ( s_LDAPSearch && s_LDAPLineEdit == this ) stopLDAPLookup(); } void AddresseeLineEdit::setFont( const TQFont& font ) { KLineEdit::setFont( font ); if ( m_useCompletion ) completionBox()->setFont( font ); } void AddresseeLineEdit::allowSemiColonAsSeparator( bool useSemiColonAsSeparator ) { m_useSemiColonAsSeparator = useSemiColonAsSeparator; } void AddresseeLineEdit::allowDistributionLists( bool allowDistLists ) { m_allowDistLists = allowDistLists; } void AddresseeLineEdit::keyPressEvent( TQKeyEvent *e ) { bool accept = false; if ( KStdAccel::shortcut( KStdAccel::SubstringCompletion ).contains( KKey( e ) ) ) { //TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch updateSearchString(); doCompletion( true ); accept = true; } else if ( KStdAccel::shortcut( KStdAccel::TextCompletion ).contains( KKey( e ) ) ) { int len = text().length(); if ( len == cursorPosition() ) { // at End? updateSearchString(); doCompletion( true ); accept = true; } } const TQString oldContent = text(); if ( !accept ) KLineEdit::keyPressEvent( e ); // if the text didn't change (eg. because a cursor navigation key was pressed) // we don't need to trigger a new search if ( oldContent == text() ) return; if ( e->isAccepted() ) { updateSearchString(); TQString searchString( m_searchString ); //LDAP does not know about our string manipulation, remove it if ( m_searchExtended ) searchString = m_searchString.mid( 1 ); if ( m_useCompletion && s_LDAPTimer != NULL ) { if ( *s_LDAPText != searchString || s_LDAPLineEdit != this ) stopLDAPLookup(); *s_LDAPText = searchString; s_LDAPLineEdit = this; s_LDAPTimer->start( 500, true ); } } } void AddresseeLineEdit::insert( const TQString &t ) { if ( !m_smartPaste ) { KLineEdit::insert( t ); return; } //kdDebug(5300) << " AddresseeLineEdit::insert( \"" << t << "\" )" << endl; TQString newText = t.stripWhiteSpace(); if ( newText.isEmpty() ) return; // remove newlines in the to-be-pasted string TQStringList lines = TQStringList::split( TQRegExp("\r?\n"), newText, false ); for ( TQStringList::iterator it = lines.begin(); it != lines.end(); ++it ) { // remove trailing commas and whitespace (*it).remove( TQRegExp(",?\\s*$") ); } newText = lines.join( ", " ); if ( newText.startsWith("mailto:") ) { KURL url( newText ); newText = url.path(); } else if ( newText.find(" at ") != -1 ) { // Anti-spam stuff newText.replace( " at ", "@" ); newText.replace( " dot ", "." ); } else if ( newText.find("(at)") != -1 ) { newText.replace( TQRegExp("\\s*\\(at\\)\\s*"), "@" ); } TQString contents = text(); int start_sel = 0; int pos = cursorPosition( ); if ( hasSelectedText() ) { // Cut away the selection. start_sel = selectionStart(); pos = start_sel; contents = contents.left( start_sel ) + contents.mid( start_sel + selectedText().length() ); } int eot = contents.length(); while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() ) { eot--; } if ( eot == 0 ) { contents = TQString(); } else if ( pos >= eot ) { if ( contents[ eot - 1 ] == ',' ) { eot--; } contents.truncate( eot ); contents += ", "; pos = eot + 2; } contents = contents.left( pos ) + newText + contents.mid( pos ); setText( contents ); setEdited( true ); setCursorPosition( pos + newText.length() ); } void AddresseeLineEdit::setText( const TQString & text ) { ClickLineEdit::setText( text.stripWhiteSpace() ); } void AddresseeLineEdit::paste() { if ( m_useCompletion ) m_smartPaste = true; KLineEdit::paste(); m_smartPaste = false; } void AddresseeLineEdit::mouseReleaseEvent( TQMouseEvent *e ) { // reimplemented from TQLineEdit::mouseReleaseEvent() if ( m_useCompletion && TQApplication::clipboard()->supportsSelection() && !isReadOnly() && e->button() == Qt::MidButton ) { m_smartPaste = true; } KLineEdit::mouseReleaseEvent( e ); m_smartPaste = false; } void AddresseeLineEdit::dropEvent( TQDropEvent *e ) { KURL::List uriList; if ( !isReadOnly() ) { if ( KURLDrag::canDecode(e) && KURLDrag::decode( e, uriList ) ) { TQString contents = text(); // remove trailing white space and comma int eot = contents.length(); while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() ) eot--; if ( eot == 0 ) contents = TQString(); else if ( contents[ eot - 1 ] == ',' ) { eot--; contents.truncate( eot ); } bool mailtoURL = false; // append the mailto URLs for ( KURL::List::Iterator it = uriList.begin(); it != uriList.end(); ++it ) { if ( !contents.isEmpty() ) contents.append( ", " ); KURL u( *it ); if ( u.protocol() == "mailto" ) { mailtoURL = true; contents.append( (*it).path() ); } } if ( mailtoURL ) { setText( contents ); setEdited( true ); return; } } else { // Let's see if this drop contains a comma separated list of emails TQString dropData = TQString::fromUtf8( e->encodedData( "text/plain" ) ); TQStringList addrs = splitEmailAddrList( dropData ); if ( addrs.count() > 0 ) { setText( normalizeAddressesAndDecodeIDNs( dropData ) ); setEdited( true ); return; } } } if ( m_useCompletion ) m_smartPaste = true; TQLineEdit::dropEvent( e ); m_smartPaste = false; } void AddresseeLineEdit::cursorAtEnd() { setCursorPosition( text().length() ); } void AddresseeLineEdit::enableCompletion( bool enable ) { m_useCompletion = enable; } void AddresseeLineEdit::doCompletion( bool ctrlT ) { m_lastSearchMode = ctrlT; TDEGlobalSettings::Completion mode = completionMode(); if ( mode == TDEGlobalSettings::CompletionNone ) return; if ( s_addressesDirty ) { loadContacts(); // read from local address book s_completion->setOrder( completionOrder() ); } // cursor at end of string - or Ctrl+T pressed for substring completion? if ( ctrlT ) { const TQStringList completions = getAdjustedCompletionItems( false ); if ( completions.count() > 1 ) ; //m_previousAddresses = prevAddr; else if ( completions.count() == 1 ) setText( m_previousAddresses + completions.first().stripWhiteSpace() ); setCompletedItems( completions, true ); // this makes sure the completion popup is closed if no matching items were found cursorAtEnd(); setCompletionMode( mode ); //set back to previous mode return; } switch ( mode ) { case TDEGlobalSettings::CompletionPopupAuto: { if ( m_searchString.isEmpty() ) break; } case TDEGlobalSettings::CompletionPopup: { const TQStringList items = getAdjustedCompletionItems( true ); setCompletedItems( items, false ); break; } case TDEGlobalSettings::CompletionShell: { TQString match = s_completion->makeCompletion( m_searchString ); if ( !match.isNull() && match != m_searchString ) { setText( m_previousAddresses + match ); setEdited( true ); cursorAtEnd(); } break; } case TDEGlobalSettings::CompletionMan: // Short-Auto in fact case TDEGlobalSettings::CompletionAuto: { //force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect setCompletionMode( completionMode() ); if ( !m_searchString.isEmpty() ) { //if only our \" is left, remove it since user has not typed it either if ( m_searchExtended && m_searchString == "\"" ){ m_searchExtended = false; m_searchString = TQString(); setText( m_previousAddresses ); break; } TQString match = s_completion->makeCompletion( m_searchString ); if ( !match.isEmpty() ) { if ( match != m_searchString ) { TQString adds = m_previousAddresses + match; setCompletedText( adds ); } } else { if ( !m_searchString.startsWith( "\"" ) ) { //try with quoted text, if user has not type one already match = s_completion->makeCompletion( "\"" + m_searchString ); if ( !match.isEmpty() && match != m_searchString ) { m_searchString = "\"" + m_searchString; m_searchExtended = true; setText( m_previousAddresses + m_searchString ); setCompletedText( m_previousAddresses + match ); } } else if ( m_searchExtended ) { //our added \" does not work anymore, remove it m_searchString = m_searchString.mid( 1 ); m_searchExtended = false; setText( m_previousAddresses + m_searchString ); //now try again match = s_completion->makeCompletion( m_searchString ); if ( !match.isEmpty() && match != m_searchString ) { TQString adds = m_previousAddresses + match; setCompletedText( adds ); } } } } break; } case TDEGlobalSettings::CompletionNone: default: // fall through break; } } void AddresseeLineEdit::slotPopupCompletion( const TQString& completion ) { setText( m_previousAddresses + completion.stripWhiteSpace() ); cursorAtEnd(); // slotMatched( m_previousAddresses + completion ); updateSearchString(); } void AddresseeLineEdit::slotReturnPressed( const TQString& item ) { Q_UNUSED( item ); TQListBoxItem* i = completionBox()->selectedItem(); if ( i != 0 ) slotPopupCompletion( i->text() ); } void AddresseeLineEdit::loadContacts() { s_completion->clear(); s_completionItemMap->clear(); s_addressesDirty = false; //m_contactMap.clear(); TQApplication::setOverrideCursor( KCursor::waitCursor() ); // loading might take a while TDEConfig config( "kpimcompletionorder" ); // The weights for non-imap kabc resources is there. config.setGroup( "CompletionWeights" ); KABC::AddressBook *addressBook = KABC::StdAddressBook::self( true ); // Can't just use the addressbook's iterator, we need to know which subresource // is behind which contact. TQPtrList<KABC::Resource> resources( addressBook->resources() ); for( TQPtrListIterator<KABC::Resource> resit( resources ); *resit; ++resit ) { KABC::Resource* resource = *resit; KPIM::ResourceABC* resabc = dynamic_cast<ResourceABC *>( resource ); if ( resabc ) { // IMAP KABC resource; need to associate each contact with the subresource const TQMap<TQString, TQString> uidToResourceMap = resabc->uidToResourceMap(); KABC::Resource::Iterator it; for ( it = resource->begin(); it != resource->end(); ++it ) { TQString uid = (*it).uid(); TQMap<TQString, TQString>::const_iterator wit = uidToResourceMap.find( uid ); const TQString subresourceLabel = resabc->subresourceLabel( *wit ); const int weight = ( wit != uidToResourceMap.end() ) ? resabc->subresourceCompletionWeight( *wit ) : 80; const int idx = addCompletionSource( subresourceLabel, weight ); //kdDebug(5300) << (*it).fullEmail() << " subres=" << *wit << " weight=" << weight << endl; addContact( *it, weight, idx ); } } else { // KABC non-imap resource int weight = config.readNumEntry( resource->identifier(), 60 ); int sourceIndex = addCompletionSource( resource->resourceName(), weight ); KABC::Resource::Iterator it; for ( it = resource->begin(); it != resource->end(); ++it ) { addContact( *it, weight, sourceIndex ); } } } #ifndef TDEPIM_NEW_DISTRLISTS // new distr lists are normal contact, already done above int weight = config.readNumEntry( "DistributionLists", 60 ); KABC::DistributionListManager manager( addressBook ); manager.load(); const TQStringList distLists = manager.listNames(); TQStringList::const_iterator listIt; int idx = addCompletionSource( i18n( "Distribution Lists" ) ); for ( listIt = distLists.begin(); listIt != distLists.end(); ++listIt ) { //for TDEGlobalSettings::CompletionAuto addCompletionItem( (*listIt).simplifyWhiteSpace(), weight, idx ); //for CompletionShell, CompletionPopup TQStringList sl( (*listIt).simplifyWhiteSpace() ); addCompletionItem( (*listIt).simplifyWhiteSpace(), weight, idx, &sl ); } #endif TQApplication::restoreOverrideCursor(); if ( !m_addressBookConnected ) { connect( addressBook, TQT_SIGNAL( addressBookChanged( AddressBook* ) ), TQT_SLOT( loadContacts() ) ); m_addressBookConnected = true; } } void AddresseeLineEdit::addContact( const KABC::Addressee& addr, int weight, int source ) { #ifdef TDEPIM_NEW_DISTRLISTS if ( KPIM::DistributionList::isDistributionList( addr ) ) { //kdDebug(5300) << "AddresseeLineEdit::addContact() distribution list \"" << addr.formattedName() << "\" weight=" << weight << endl; if ( m_allowDistLists ) { //for CompletionAuto addCompletionItem( addr.formattedName(), weight, source ); //for CompletionShell, CompletionPopup TQStringList sl( addr.formattedName() ); addCompletionItem( addr.formattedName(), weight, source, &sl ); } return; } #endif //m_contactMap.insert( addr.realName(), addr ); const TQStringList emails = addr.emails(); TQStringList::ConstIterator it; const int prefEmailWeight = 1; //increment weight by prefEmailWeight int isPrefEmail = prefEmailWeight; //first in list is preferredEmail for ( it = emails.begin(); it != emails.end(); ++it ) { //TODO: highlight preferredEmail const TQString email( (*it) ); const TQString givenName = addr.givenName(); const TQString familyName= addr.familyName(); const TQString nickName = addr.nickName(); const TQString domain = email.mid( email.find( '@' ) + 1 ); TQString fullEmail = addr.fullEmail( email ); //TODO: let user decide what fields to use in lookup, e.g. company, city, ... //for CompletionAuto if ( givenName.isEmpty() && familyName.isEmpty() ) { addCompletionItem( fullEmail, weight + isPrefEmail, source ); // use whatever is there } else { const TQString byFirstName= "\"" + givenName + " " + familyName + "\" <" + email + ">"; const TQString byLastName = "\"" + familyName + ", " + givenName + "\" <" + email + ">"; addCompletionItem( byFirstName, weight + isPrefEmail, source ); addCompletionItem( byLastName, weight + isPrefEmail, source ); } addCompletionItem( email, weight + isPrefEmail, source ); if ( !nickName.isEmpty() ){ const TQString byNick = "\"" + nickName + "\" <" + email + ">"; addCompletionItem( byNick, weight + isPrefEmail, source ); } if ( !domain.isEmpty() ){ const TQString byDomain = "\"" + domain + " " + familyName + " " + givenName + "\" <" + email + ">"; addCompletionItem( byDomain, weight + isPrefEmail, source ); } //for CompletionShell, CompletionPopup TQStringList keyWords; const TQString realName = addr.realName(); if ( !givenName.isEmpty() && !familyName.isEmpty() ) { keyWords.append( givenName + " " + familyName ); keyWords.append( familyName + " " + givenName ); keyWords.append( familyName + ", " + givenName); }else if ( !givenName.isEmpty() ) keyWords.append( givenName ); else if ( !familyName.isEmpty() ) keyWords.append( familyName ); if ( !nickName.isEmpty() ) keyWords.append( nickName ); if ( !realName.isEmpty() ) keyWords.append( realName ); if ( !domain.isEmpty() ) keyWords.append( domain ); keyWords.append( email ); /* KMailCompletion does not have knowledge about identities, it stores emails and * keywords for each email. KMailCompletion::allMatches does a lookup on the * keywords and returns an ordered list of emails. In order to get the preferred * email before others for each identity we use this little trick. * We remove the <blank> in getAdjustedCompletionItems. */ if ( isPrefEmail == prefEmailWeight ) fullEmail.replace( " <", " <" ); addCompletionItem( fullEmail, weight + isPrefEmail, source, &keyWords ); isPrefEmail = 0; #if 0 int len = (*it).length(); if ( len == 0 ) continue; if( '\0' == (*it)[len-1] ) --len; const TQString tmp = (*it).left( len ); const TQString fullEmail = addr.fullEmail( tmp ); //kdDebug(5300) << "AddresseeLineEdit::addContact() \"" << fullEmail << "\" weight=" << weight << endl; addCompletionItem( fullEmail.simplifyWhiteSpace(), weight, source ); // Try to guess the last name: if found, we add an extra // entry to the list to make sure completion works even // if the user starts by typing in the last name. TQString name( addr.realName().simplifyWhiteSpace() ); if( name.endsWith("\"") ) name.truncate( name.length()-1 ); if( name.startsWith("\"") ) name = name.mid( 1 ); // While we're here also add "email (full name)" for completion on the email if ( !name.isEmpty() ) addCompletionItem( addr.preferredEmail() + " (" + name + ")", weight, source ); bool bDone = false; int i = -1; while( ( i = name.findRev(' ') ) > 1 && !bDone ) { TQString sLastName( name.mid( i+1 ) ); if( ! sLastName.isEmpty() && 2 <= sLastName.length() && // last names must be at least 2 chars long ! sLastName.endsWith(".") ) { // last names must not end with a dot (like "Jr." or "Sr.") name.truncate( i ); if( !name.isEmpty() ){ sLastName.prepend( "\"" ); sLastName.append( ", " + name + "\" <" ); } TQString sExtraEntry( sLastName ); sExtraEntry.append( tmp.isEmpty() ? addr.preferredEmail() : tmp ); sExtraEntry.append( ">" ); //kdDebug(5300) << "AddresseeLineEdit::addContact() added extra \"" << sExtraEntry.simplifyWhiteSpace() << "\" weight=" << weight << endl; addCompletionItem( sExtraEntry.simplifyWhiteSpace(), weight, source ); bDone = true; } if( !bDone ) { name.truncate( i ); if( name.endsWith("\"") ) name.truncate( name.length()-1 ); } } #endif } } void AddresseeLineEdit::addCompletionItem( const TQString& string, int weight, int completionItemSource, const TQStringList * keyWords ) { // Check if there is an exact match for item already, and use the max weight if so. // Since there's no way to get the information from KCompletion, we have to keep our own TQMap CompletionItemsMap::iterator it = s_completionItemMap->find( string ); if ( it != s_completionItemMap->end() ) { weight = TQMAX( ( *it ).first, weight ); ( *it ).first = weight; } else { s_completionItemMap->insert( string, tqMakePair( weight, completionItemSource ) ); } if ( keyWords == 0 ) s_completion->addItem( string, weight ); else s_completion->addItemWithKeys( string, weight, keyWords ); } void AddresseeLineEdit::slotStartLDAPLookup() { TDEGlobalSettings::Completion mode = completionMode(); if ( mode == TDEGlobalSettings::CompletionNone ) return; if ( !s_LDAPSearch->isAvailable() ) { return; } if ( s_LDAPLineEdit != this ) return; startLoadingLDAPEntries(); } void AddresseeLineEdit::stopLDAPLookup() { s_LDAPSearch->cancelSearch(); s_LDAPLineEdit = NULL; } void AddresseeLineEdit::startLoadingLDAPEntries() { TQString s( *s_LDAPText ); // TODO cache last? TQString prevAddr; int n = s.findRev( ',' ); if ( n >= 0 ) { prevAddr = s.left( n + 1 ) + ' '; s = s.mid( n + 1, 255 ).stripWhiteSpace(); } if ( s.isEmpty() ) return; //loadContacts(); // TODO reuse these? s_LDAPSearch->startSearch( s ); } void AddresseeLineEdit::slotLDAPSearchData( const KPIM::LdapResultList& adrs ) { if ( adrs.isEmpty() || s_LDAPLineEdit != this ) return; for ( KPIM::LdapResultList::ConstIterator it = adrs.begin(); it != adrs.end(); ++it ) { KABC::Addressee addr; addr.setNameFromString( (*it).name ); addr.setEmails( (*it).email ); if ( !s_ldapClientToCompletionSourceMap->contains( (*it).clientNumber ) ) updateLDAPWeights(); // we got results from a new source, so update the completion sources addContact( addr, (*it).completionWeight, (*s_ldapClientToCompletionSourceMap)[ (*it ).clientNumber ] ); } if ( (hasFocus() || completionBox()->hasFocus() ) && completionMode() != TDEGlobalSettings::CompletionNone && completionMode() != TDEGlobalSettings::CompletionShell ) { setText( m_previousAddresses + m_searchString ); // only complete again if the user didn't change the selection while we were waiting // otherwise the completion box will be closed if ( m_searchString.stripWhiteSpace() != completionBox()->currentText().stripWhiteSpace() ) doCompletion( m_lastSearchMode ); } } void AddresseeLineEdit::setCompletedItems( const TQStringList& items, bool autoSuggest ) { KCompletionBox* completionBox = this->completionBox(); if ( !items.isEmpty() && !(items.count() == 1 && m_searchString == items.first()) ) { TQString oldCurrentText = completionBox->currentText(); TQListBoxItem *itemUnderMouse = completionBox->itemAt( completionBox->viewport()->mapFromGlobal(TQCursor::pos()) ); TQString oldTextUnderMouse; TQPoint oldPosOfItemUnderMouse; if ( itemUnderMouse ) { oldTextUnderMouse = itemUnderMouse->text(); oldPosOfItemUnderMouse = completionBox->itemRect( itemUnderMouse ).topLeft(); } completionBox->setItems( items ); if ( !completionBox->isVisible() ) { if ( !m_searchString.isEmpty() ) completionBox->setCancelledText( m_searchString ); completionBox->popup(); // we have to install the event filter after popup(), since that // calls show(), and that's where KCompletionBox installs its filter. // We want to be first, though, so do it now. if ( s_completion->order() == KCompletion::Weighted ) tqApp->installEventFilter( this ); } // Try to re-select what was selected before, otherrwise use the first // item, if there is one TQListBoxItem* item = 0; if ( oldCurrentText.isEmpty() || ( item = completionBox->findItem( oldCurrentText ) ) == 0 ) { item = completionBox->item( 1 ); } if ( item ) { if ( itemUnderMouse ) { TQListBoxItem *newItemUnderMouse = completionBox->findItem( oldTextUnderMouse ); // if the mouse was over an item, before, but now that's elsewhere, // move the cursor, so folks don't accidently click the wrong item if ( newItemUnderMouse ) { TQRect r = completionBox->itemRect( newItemUnderMouse ); TQPoint target = r.topLeft(); if ( oldPosOfItemUnderMouse != target ) { target.setX( target.x() + r.width()/2 ); TQCursor::setPos( completionBox->viewport()->mapToGlobal(target) ); } } } completionBox->blockSignals( true ); completionBox->setSelected( item, true ); completionBox->setCurrentItem( item ); completionBox->ensureCurrentVisible(); completionBox->blockSignals( false ); } if ( autoSuggest ) { int index = items.first().find( m_searchString ); TQString newText = items.first().mid( index ); setUserSelection(false); setCompletedText(newText,true); } } else { if ( completionBox && completionBox->isVisible() ) { completionBox->hide(); completionBox->setItems( TQStringList() ); } } } TQPopupMenu* AddresseeLineEdit::createPopupMenu() { TQPopupMenu *menu = KLineEdit::createPopupMenu(); if ( !menu ) return 0; if ( m_useCompletion ){ menu->setItemVisible( ShortAutoCompletion, false ); menu->setItemVisible( PopupAutoCompletion, false ); menu->insertItem( i18n( "Configure Completion Order..." ), this, TQT_SLOT( slotEditCompletionOrder() ) ); } return menu; } void AddresseeLineEdit::slotEditCompletionOrder() { init(); // for s_LDAPSearch CompletionOrderEditor editor( s_LDAPSearch, this ); editor.exec(); if ( m_useCompletion ) { updateLDAPWeights(); s_addressesDirty = true; } } void KPIM::AddresseeLineEdit::slotIMAPCompletionOrderChanged() { if ( m_useCompletion ) s_addressesDirty = true; } void KPIM::AddresseeLineEdit::slotUserCancelled( const TQString& cancelText ) { if ( s_LDAPSearch && s_LDAPLineEdit == this ) stopLDAPLookup(); userCancelled( m_previousAddresses + cancelText ); // in KLineEdit } void AddresseeLineEdit::updateSearchString() { m_searchString = text(); int n = -1; bool inQuote = false; uint searchStringLength = m_searchString.length(); for ( uint i = 0; i < searchStringLength; ++i ) { if ( m_searchString[ i ] == '"' ) { inQuote = !inQuote; } if ( m_searchString[ i ] == '\\' && (i + 1) < searchStringLength && m_searchString[ i + 1 ] == '"' ) { ++i; } if ( inQuote ) { continue; } if ( i < searchStringLength && ( m_searchString[ i ] == ',' || ( m_useSemiColonAsSeparator && m_searchString[ i ] == ';' ) ) ) { n = i; } } if ( n >= 0 ) { ++n; // Go past the "," int len = m_searchString.length(); // Increment past any whitespace... while ( n < len && m_searchString[ n ].isSpace() ) ++n; m_previousAddresses = m_searchString.left( n ); m_searchString = m_searchString.mid( n ).stripWhiteSpace(); } else { m_previousAddresses = TQString(); } } void KPIM::AddresseeLineEdit::slotCompletion() { // Called by KLineEdit's keyPressEvent for CompletionModes Auto,Popup -> new text, update search string // not called for CompletionShell, this is been taken care of in AddresseeLineEdit::keyPressEvent updateSearchString(); if ( completionBox() ) completionBox()->setCancelledText( m_searchString ); doCompletion( false ); } // not cached, to make sure we get an up-to-date value when it changes KCompletion::CompOrder KPIM::AddresseeLineEdit::completionOrder() { TDEConfig config( "kpimcompletionorder" ); config.setGroup( "General" ); const TQString order = config.readEntry( "CompletionOrder", "Weighted" ); if ( order == "Weighted" ) return KCompletion::Weighted; else return KCompletion::Sorted; } int KPIM::AddresseeLineEdit::addCompletionSource( const TQString &source, int weight ) { TQMap<TQString,int>::iterator it = s_completionSourceWeights->find( source ); if ( it == s_completionSourceWeights->end() ) s_completionSourceWeights->insert( source, weight ); else (*s_completionSourceWeights)[source] = weight; int sourceIndex = s_completionSources->findIndex( source ); if ( sourceIndex == -1 ) { s_completionSources->append( source ); return s_completionSources->size() - 1; } else return sourceIndex; } bool KPIM::AddresseeLineEdit::eventFilter(TQObject *obj, TQEvent *e) { if ( TQT_BASE_OBJECT(obj) == TQT_BASE_OBJECT(completionBox()) ) { if ( e->type() == TQEvent::MouseButtonPress || e->type() == TQEvent::MouseMove || e->type() == TQEvent::MouseButtonRelease || e->type() == TQEvent::MouseButtonDblClick ) { TQMouseEvent* me = TQT_TQMOUSEEVENT( e ); // find list box item at the event position TQListBoxItem *item = completionBox()->itemAt( me->pos() ); if ( !item ) { // In the case of a mouse move outside of the box we don't want // the parent to fuzzy select a header by mistake. bool eat = e->type() == TQEvent::MouseMove; return eat; } // avoid selection of headers on button press, or move or release while // a button is pressed if ( e->type() == TQEvent::MouseButtonPress || me->state() & Qt::LeftButton || me->state() & Qt::MidButton || me->state() & Qt::RightButton ) { if ( itemIsHeader(item) ) { return true; // eat the event, we don't want anything to happen } else { // if we are not on one of the group heading, make sure the item // below or above is selected, not the heading, inadvertedly, due // to fuzzy auto-selection from TQListBox completionBox()->setCurrentItem( item ); completionBox()->setSelected( completionBox()->index( item ), true ); if ( e->type() == TQEvent::MouseMove ) return true; // avoid fuzzy selection behavior } } } } if ( ( TQT_BASE_OBJECT(obj) == TQT_BASE_OBJECT(this) ) && ( e->type() == TQEvent::AccelOverride ) ) { TQKeyEvent *ke = TQT_TQKEYEVENT( e ); if ( ke->key() == Key_Up || ke->key() == Key_Down || ke->key() == Key_Tab ) { ke->accept(); return true; } } if ( ( TQT_BASE_OBJECT(obj) == TQT_BASE_OBJECT(this) ) && ( e->type() == TQEvent::KeyPress || e->type() == TQEvent::KeyRelease ) && completionBox()->isVisible() ) { TQKeyEvent *ke = TQT_TQKEYEVENT( e ); int currentIndex = completionBox()->currentItem(); if ( currentIndex < 0 ) { return true; } if ( ke->key() == Key_Up ) { //kdDebug() << "EVENTFILTER: Key_Up currentIndex=" << currentIndex << endl; // figure out if the item we would be moving to is one we want // to ignore. If so, go one further TQListBoxItem *itemAbove = completionBox()->item( currentIndex ); if ( itemAbove && itemIsHeader(itemAbove) ) { // there is a header above us, check if there is even further up // and if so go one up, so it'll be selected if ( currentIndex > 0 && completionBox()->item( currentIndex - 1 ) ) { //kdDebug() << "EVENTFILTER: Key_Up -> skipping " << currentIndex - 1 << endl; completionBox()->setCurrentItem( itemAbove->prev() ); completionBox()->setSelected( currentIndex - 1, true ); } else if ( currentIndex == 0 ) { // nothing to skip to, let's stay where we are, but make sure the // first header becomes visible, if we are the first real entry completionBox()->ensureVisible( 0, 0 ); //Kolab issue 2941: be sure to add email even if it's the only element. if ( itemIsHeader( completionBox()->item( currentIndex ) ) ) { currentIndex++; } completionBox()->setCurrentItem( itemAbove ); completionBox()->setSelected( currentIndex, true ); } return true; } } else if ( ke->key() == Key_Down ) { // same strategy for downwards //kdDebug() << "EVENTFILTER: Key_Down. currentIndex=" << currentIndex << endl; TQListBoxItem *itemBelow = completionBox()->item( currentIndex ); if ( itemBelow && itemIsHeader( itemBelow ) ) { if ( completionBox()->item( currentIndex + 1 ) ) { //kdDebug() << "EVENTFILTER: Key_Down -> skipping " << currentIndex+1 << endl; completionBox()->setCurrentItem( itemBelow->next() ); completionBox()->setSelected( currentIndex + 1, true ); } else { // nothing to skip to, let's stay where we are completionBox()->setCurrentItem( itemBelow ); completionBox()->setSelected( currentIndex, true ); } return true; } // special case of the last and only item in the list needing selection if ( !itemBelow && currentIndex == 1 ) { completionBox()->setSelected( currentIndex, true ); } // special case of the initial selection, which is unfortunately a header. // Setting it to selected tricks KCompletionBox into not treating is special // and selecting making it current, instead of the one below. TQListBoxItem *item = completionBox()->item( currentIndex ); if ( item && itemIsHeader(item) ) { completionBox()->setSelected( currentIndex, true ); } } else if ( e->type() == TQEvent::KeyRelease && ( ke->key() == Key_Tab || ke->key() == Key_Backtab ) ) { //kdDebug() << "EVENTFILTER: Key_Tab. currentIndex=" << currentIndex << endl; /// first, find the header of the current section TQListBoxItem *myHeader = 0; const int iterationstep = ke->key() == Key_Tab ? 1 : -1; int i = TQMIN( TQMAX( currentIndex - iterationstep, 0 ), completionBox()->count() - 1 ); while ( i>=0 ) { if ( itemIsHeader( completionBox()->item(i) ) ) { myHeader = completionBox()->item( i ); break; } i--; } Q_ASSERT( myHeader ); // we should always be able to find a header // find the next header (searching backwards, for Key_Backtab) TQListBoxItem *nextHeader = 0; // when iterating forward, start at the currentindex, when backwards, // one up from our header, or at the end uint j; if ( ke->key() == Key_Tab ) { j = currentIndex; } else { i = completionBox()->index( myHeader ); if ( i == 0 ) { j = completionBox()->count() - 1; } else { j = ( i - 1 ) % completionBox()->count(); } } while ( ( nextHeader = completionBox()->item( j ) ) && nextHeader != myHeader ) { if ( itemIsHeader(nextHeader) ) { break; } j = (j + iterationstep) % completionBox()->count(); } if ( nextHeader && nextHeader != myHeader ) { TQListBoxItem *item = completionBox()->item( j + 1 ); if ( item && !itemIsHeader(item) ) { completionBox()->setSelected( item, true ); completionBox()->setCurrentItem( item ); completionBox()->ensureCurrentVisible(); } } return true; } } return ClickLineEdit::eventFilter( obj, e ); } class SourceWithWeight { public: int weight; // the weight of the source TQString sourceName; // the name of the source, e.g. "LDAP Server" int index; // index into s_completionSources bool operator< ( const SourceWithWeight &other ) { if ( weight > other.weight ) return true; if ( weight < other.weight ) return false; return sourceName < other.sourceName; } }; const TQStringList KPIM::AddresseeLineEdit::getAdjustedCompletionItems( bool fullSearch ) { TQStringList items = fullSearch ? s_completion->allMatches( m_searchString ) : s_completion->substringCompletion( m_searchString ); // For weighted mode, the algorithm is the following: // In the first loop, we add each item to its section (there is one section per completion source) // We also add spaces in front of the items. // The sections are appended to the items list. // In the second loop, we then walk through the sections and add all the items in there to the // sorted item list, which is the final result. // // The algo for non-weighted mode is different. int lastSourceIndex = -1; unsigned int i = 0; // Maps indices of the items list, which are section headers/source items, // to a TQStringList which are the items of that section/source. TQMap<int, TQStringList> sections; TQStringList sortedItems; for ( TQStringList::Iterator it = items.begin(); it != items.end(); ++it, ++i ) { CompletionItemsMap::const_iterator cit = s_completionItemMap->find(*it); if ( cit == s_completionItemMap->end() ) continue; int idx = (*cit).second; if ( s_completion->order() == KCompletion::Weighted ) { if ( lastSourceIndex == -1 || lastSourceIndex != idx ) { const TQString sourceLabel( (*s_completionSources)[idx] ); if ( sections.find(idx) == sections.end() ) { items.insert( it, sourceLabel ); } lastSourceIndex = idx; } (*it) = (*it).prepend( s_completionItemIndentString ); // remove preferred email sort <blank> added in addContact() (*it).replace( " <", " <" ); } sections[idx].append( *it ); if ( s_completion->order() == KCompletion::Sorted ) { sortedItems.append( *it ); } } if ( s_completion->order() == KCompletion::Weighted ) { // Sort the sections TQValueList<SourceWithWeight> sourcesAndWeights; for ( uint i = 0; i < s_completionSources->size(); i++ ) { SourceWithWeight sww; sww.sourceName = (*s_completionSources)[i]; sww.weight = (*s_completionSourceWeights)[sww.sourceName]; sww.index = i; sourcesAndWeights.append( sww ); } qHeapSort( sourcesAndWeights ); // Add the sections and their items to the final sortedItems result list for( uint i = 0; i < sourcesAndWeights.size(); i++ ) { TQStringList sectionItems = sections[sourcesAndWeights[i].index]; if ( !sectionItems.isEmpty() ) { sortedItems.append( sourcesAndWeights[i].sourceName ); TQStringList sectionItems = sections[sourcesAndWeights[i].index]; for ( TQStringList::Iterator sit( sectionItems.begin() ), send( sectionItems.end() ); sit != send; ++sit ) { sortedItems.append( *sit ); } } } } else { sortedItems.sort(); } return sortedItems; } #include "addresseelineedit.moc"