krop's commit fixes my problem in a better way, reverting
[kdepim.git] / libkdepim / addresseelineedit.cpp
blob0146603be368169daa06a0d34b5cf52c34e81e01
1 /*
2 This file is part of libkdepim.
4 Copyright (c) 2002 Helge Deller <deller@gmx.de>
5 Copyright (c) 2002 Lubos Lunak <llunak@suse.cz>
6 Copyright (c) 2001,2003 Carsten Pfeiffer <pfeiffer@kde.org>
7 Copyright (c) 2001 Waldo Bastian <bastian@kde.org>
8 Copyright (c) 2004 Daniel Molkentin <danimo@klaralvdalens-datakonsult.se>
9 Copyright (c) 2004 Karl-Heinz Zimmer <khz@klaralvdalens-datakonsult.se>
11 This library is free software; you can redistribute it and/or
12 modify it under the terms of the GNU Library General Public
13 License as published by the Free Software Foundation; either
14 version 2 of the License, or (at your option) any later version.
16 This library is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 Library General Public License for more details.
21 You should have received a copy of the GNU Library General Public License
22 along with this library; see the file COPYING.LIB. If not, write to
23 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 Boston, MA 02110-1301, USA.
27 #include "addresseelineedit.h"
28 #include "completionordereditor.h"
29 #include "ldapclient.h"
30 #include "distributionlist.h"
32 #include <kabc/stdaddressbook.h>
33 #include <kabc/resource.h>
34 #include <kabc/resourceabc.h>
35 #include <kmime/kmime_util.h>
37 #include <KCompletionBox>
38 #include <KDebug>
39 #include <KLocale>
40 #include <KStandardDirs>
41 #include <KStandardShortcut>
42 #include <KUrl>
43 #include <K3StaticDeleter>
45 #include <QApplication>
46 #include <QCursor>
47 #include <QObject>
48 #include <QRegExp>
49 #include <QEvent>
50 #include <QClipboard>
51 #include <QKeyEvent>
52 #include <QDropEvent>
53 #include <QMouseEvent>
54 #include <QMenu>
55 #include <QtDBus/QDBusConnection>
57 using namespace KPIM;
59 KMailCompletion *AddresseeLineEdit::s_completion = 0;
60 KPIM::CompletionItemsMap *AddresseeLineEdit::s_completionItemMap = 0;
61 QStringList *AddresseeLineEdit::s_completionSources = 0;
62 bool AddresseeLineEdit::s_addressesDirty = false;
63 QTimer *AddresseeLineEdit::s_LDAPTimer = 0;
64 KPIM::LdapSearch *AddresseeLineEdit::s_LDAPSearch = 0;
65 QString *AddresseeLineEdit::s_LDAPText = 0;
66 AddresseeLineEdit *AddresseeLineEdit::s_LDAPLineEdit = 0;
68 // The weights associated with the completion sources in s_completionSources.
69 // Both are maintained by addCompletionSource(), don't attempt to modifiy those yourself.
70 QMap<QString,int>* s_completionSourceWeights = 0;
72 // maps LDAP client indices to completion source indices
73 // the assumption that they are always the first n indices in s_completion
74 // does not hold when clients are added later on
75 QMap<int, int>* AddresseeLineEdit::s_ldapClientToCompletionSourceMap = 0;
77 static K3StaticDeleter<KMailCompletion> completionDeleter;
78 static K3StaticDeleter<KPIM::CompletionItemsMap> completionItemsDeleter;
79 static K3StaticDeleter<QTimer> ldapTimerDeleter;
80 static K3StaticDeleter<KPIM::LdapSearch> ldapSearchDeleter;
81 static K3StaticDeleter<QString> ldapTextDeleter;
82 static K3StaticDeleter<QStringList> completionSourcesDeleter;
83 static K3StaticDeleter<QMap<QString,int> > completionSourceWeightsDeleter;
84 static K3StaticDeleter<QMap<int, int> > ldapClientToCompletionSourceMapDeleter;
86 // needs to be unique, but the actual name doesn't matter much
87 static QByteArray newLineEditObjectName()
89 static int s_count = 0;
90 QByteArray name( "KPIM::AddresseeLineEdit" );
91 if ( s_count++ ) {
92 name += '-';
93 name += QByteArray().setNum( s_count );
95 return name;
98 static const QString s_completionItemIndentString = " ";
100 static bool itemIsHeader( const QListWidgetItem *item )
102 return item && !item->text().startsWith( s_completionItemIndentString );
105 AddresseeLineEdit::AddresseeLineEdit( QWidget *parent, bool useCompletion )
106 : KLineEdit( parent )
108 setObjectName( newLineEditObjectName() );
109 setClickMessage( "" );
110 m_useCompletion = useCompletion;
111 m_completionInitialized = false;
112 m_smartPaste = false;
113 m_addressBookConnected = false;
114 m_searchExtended = false;
116 init();
118 if ( m_useCompletion ) {
119 s_addressesDirty = true;
123 void AddresseeLineEdit::updateLDAPWeights()
125 /* Add completion sources for all ldap server, 0 to n. Added first so
126 * that they map to the ldapclient::clientNumber() */
127 s_LDAPSearch->updateCompletionWeights();
128 int clientIndex = 0;
129 foreach ( const LdapClient *client, s_LDAPSearch->clients() ) {
130 const int sourceIndex = addCompletionSource(
131 "LDAP server: " + client->server().host(), client->completionWeight() );
132 s_ldapClientToCompletionSourceMap->insert( clientIndex, sourceIndex );
136 void AddresseeLineEdit::init()
138 if ( !s_completion ) {
139 completionDeleter.setObject( s_completion, new KMailCompletion() );
140 s_completion->setOrder( completionOrder() );
141 s_completion->setIgnoreCase( true );
143 completionItemsDeleter.setObject( s_completionItemMap, new KPIM::CompletionItemsMap() );
144 completionSourcesDeleter.setObject( s_completionSources, new QStringList() );
145 completionSourceWeightsDeleter.setObject( s_completionSourceWeights, new QMap<QString,int> );
146 ldapClientToCompletionSourceMapDeleter.setObject( s_ldapClientToCompletionSourceMap, new QMap<int,int> );
148 // connect( s_completion, SIGNAL(match(const QString&)),
149 // this, SLOT(slotMatched(const QString&)) );
151 if ( m_useCompletion ) {
152 if ( !s_LDAPTimer ) {
153 ldapTimerDeleter.setObject( s_LDAPTimer, new QTimer );
154 ldapSearchDeleter.setObject( s_LDAPSearch, new KPIM::LdapSearch );
155 ldapTextDeleter.setObject( s_LDAPText, new QString );
158 updateLDAPWeights();
160 if ( !m_completionInitialized ) {
161 setCompletionObject( s_completion, false );
162 connect( this, SIGNAL(completion(const QString&)),
163 this, SLOT(slotCompletion()) );
164 connect( this, SIGNAL(returnPressed(const QString&)),
165 this, SLOT(slotReturnPressed(const QString&)) );
167 KCompletionBox *box = completionBox();
168 connect( box, SIGNAL(activated(const QString&)),
169 this, SLOT(slotPopupCompletion(const QString&)) );
170 connect( box, SIGNAL(userCancelled(const QString&)),
171 SLOT(slotUserCancelled(const QString&)) );
173 // The emitter is always called KPIM::IMAPCompletionOrder by contract
174 if ( !QDBusConnection::sessionBus().connect(
175 "kde.org.pim.completionorder", "/",
176 "kde.org.pim.CompletionOrder", "completionOrderChanged",
177 this, SLOT(slotIMAPCompletionOrderChanged()) ) ) {
178 kError() << "AddresseeLineEdit: connection to orderChanged() failed";
181 connect( s_LDAPTimer, SIGNAL(timeout()), SLOT(slotStartLDAPLookup()) );
182 connect( s_LDAPSearch, SIGNAL(searchData(const KPIM::LdapResultList&)),
183 SLOT(slotLDAPSearchData(const KPIM::LdapResultList&)) );
185 m_completionInitialized = true;
190 AddresseeLineEdit::~AddresseeLineEdit()
192 if ( s_LDAPSearch && s_LDAPLineEdit == this ) {
193 stopLDAPLookup();
197 void AddresseeLineEdit::setFont( const QFont &font )
199 KLineEdit::setFont( font );
200 if ( m_useCompletion ) {
201 completionBox()->setFont( font );
205 void AddresseeLineEdit::allowSemiColonAsSeparator( bool useSemiColonAsSeparator )
207 m_useSemiColonAsSeparator = useSemiColonAsSeparator;
210 void AddresseeLineEdit::keyPressEvent( QKeyEvent *e )
212 bool accept = false;
214 const int key = e->key() | e->modifiers();
216 if ( KStandardShortcut::shortcut( KStandardShortcut::SubstringCompletion ).contains( key ) ) {
217 //TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch
218 updateSearchString();
219 doCompletion( true );
220 accept = true;
221 } else if ( KStandardShortcut::shortcut( KStandardShortcut::TextCompletion ).contains( key ) ) {
222 int len = text().length();
224 if ( len == cursorPosition() ) { // at End?
225 updateSearchString();
226 doCompletion( true );
227 accept = true;
231 const QString oldContent = text();
232 if ( !accept ) {
233 KLineEdit::keyPressEvent( e );
236 // if the text didn't change (eg. because a cursor navigation key was pressed)
237 // we don't need to trigger a new search
238 if ( oldContent == text() )
239 return;
241 if ( e->isAccepted() ) {
242 updateSearchString();
243 QString searchString( m_searchString );
244 //LDAP does not know about our string manipulation, remove it
245 if ( m_searchExtended ) {
246 searchString = m_searchString.mid( 1 );
249 if ( m_useCompletion && s_LDAPTimer ) {
250 if ( *s_LDAPText != searchString || s_LDAPLineEdit != this ) {
251 stopLDAPLookup();
254 *s_LDAPText = searchString;
255 s_LDAPLineEdit = this;
256 s_LDAPTimer->setSingleShot( true );
257 s_LDAPTimer->start( 500 );
262 void AddresseeLineEdit::insert( const QString &t )
264 if ( !m_smartPaste ) {
265 KLineEdit::insert( t );
266 return;
269 QString newText = t.trimmed();
270 if ( newText.isEmpty() ) {
271 return;
274 // remove newlines in the to-be-pasted string
275 QStringList lines = newText.split( QRegExp( "\r?\n" ), QString::SkipEmptyParts );
276 for ( QStringList::iterator it = lines.begin(); it != lines.end(); ++it ) {
277 // remove trailing commas and whitespace
278 (*it).remove( QRegExp( ",?\\s*$" ) );
280 newText = lines.join( ", " );
282 if ( newText.startsWith( QLatin1String( "mailto:" ) ) ) {
283 KUrl url( newText );
284 newText = url.path();
285 } else if ( newText.indexOf( " at " ) != -1 ) {
286 // Anti-spam stuff
287 newText.replace( " at ", "@" );
288 newText.replace( " dot ", "." );
289 } else if ( newText.indexOf( "(at)" ) != -1 ) {
290 newText.replace( QRegExp( "\\s*\\(at\\)\\s*" ), "@" );
293 QString contents = text();
294 int start_sel = 0;
295 int pos = cursorPosition( );
297 if ( hasSelectedText() ) {
298 // Cut away the selection.
299 start_sel = selectionStart();
300 pos = start_sel;
301 contents = contents.left( start_sel ) + contents.mid( start_sel + selectedText().length() );
304 int eot = contents.length();
305 while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() ) {
306 eot--;
308 if ( eot == 0 ) {
309 contents.clear();
310 } else if ( pos >= eot ) {
311 if ( contents[ eot - 1 ] == ',' ) {
312 eot--;
314 contents.truncate( eot );
315 contents += ", ";
316 pos = eot + 2;
319 contents = contents.left( pos ) + newText + contents.mid( pos );
320 setText( contents );
321 setModified( true );
322 setCursorPosition( pos + newText.length() );
325 void AddresseeLineEdit::setText( const QString & text )
327 KLineEdit::setText( text.trimmed() );
330 void AddresseeLineEdit::paste()
332 if ( m_useCompletion ) {
333 m_smartPaste = true;
336 KLineEdit::paste();
337 m_smartPaste = false;
340 void AddresseeLineEdit::mouseReleaseEvent( QMouseEvent *e )
342 // reimplemented from QLineEdit::mouseReleaseEvent()
343 if ( m_useCompletion &&
344 QApplication::clipboard()->supportsSelection() &&
345 !isReadOnly() &&
346 e->button() == Qt::MidButton ) {
347 m_smartPaste = true;
350 KLineEdit::mouseReleaseEvent( e );
351 m_smartPaste = false;
354 void AddresseeLineEdit::dropEvent( QDropEvent *e )
356 if ( !isReadOnly() ) {
357 KUrl::List uriList = KUrl::List::fromMimeData( e->mimeData() );
358 if ( !uriList.isEmpty() ) {
359 QString contents = text();
360 // remove trailing white space and comma
361 int eot = contents.length();
362 while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() ) {
363 eot--;
365 if ( eot == 0 ) {
366 contents.clear();
367 } else if ( contents[ eot - 1 ] == ',' ) {
368 eot--;
369 contents.truncate( eot );
371 bool mailtoURL = false;
372 // append the mailto URLs
373 for ( KUrl::List::Iterator it = uriList.begin(); it != uriList.end(); ++it ) {
374 KUrl u( *it );
375 if ( u.protocol() == "mailto" ) {
376 mailtoURL = true;
377 QString address;
378 address = KUrl::fromPercentEncoding( u.path().toLatin1() );
379 address = KMime::decodeRFC2047String( address.toAscii() );
380 if ( !contents.isEmpty() ) {
381 contents.append( ", " );
383 contents.append( address );
386 if ( mailtoURL ) {
387 setText( contents );
388 setModified( true );
389 return;
394 if ( m_useCompletion ) {
395 m_smartPaste = true;
397 QLineEdit::dropEvent( e );
398 m_smartPaste = false;
401 void AddresseeLineEdit::cursorAtEnd()
403 setCursorPosition( text().length() );
406 void AddresseeLineEdit::enableCompletion( bool enable )
408 m_useCompletion = enable;
411 void AddresseeLineEdit::doCompletion( bool ctrlT )
413 m_lastSearchMode = ctrlT;
415 KGlobalSettings::Completion mode = completionMode();
417 if ( mode == KGlobalSettings::CompletionNone ) {
418 return;
421 if ( s_addressesDirty ) {
422 loadContacts(); // read from local address book
423 s_completion->setOrder( completionOrder() );
426 // cursor at end of string - or Ctrl+T pressed for substring completion?
427 if ( ctrlT ) {
428 const QStringList completions = getAdjustedCompletionItems( false );
430 if ( completions.count() > 1 ) {
431 ; //m_previousAddresses = prevAddr;
432 } else if ( completions.count() == 1 ) {
433 setText( m_previousAddresses + completions.first().trimmed() );
436 // Make sure the completion popup is closed if no matching items were found
437 setCompletedItems( completions, true );
439 cursorAtEnd();
440 setCompletionMode( mode ); //set back to previous mode
441 return;
444 switch ( mode ) {
445 case KGlobalSettings::CompletionPopupAuto:
447 if ( m_searchString.isEmpty() ) {
448 break;
450 //else: fall-through to the CompletionPopup case
453 case KGlobalSettings::CompletionPopup:
455 const QStringList items = getAdjustedCompletionItems( true );
456 setCompletedItems( items, false );
457 break;
460 case KGlobalSettings::CompletionShell:
462 QString match = s_completion->makeCompletion( m_searchString );
463 if ( !match.isNull() && match != m_searchString ) {
464 setText( m_previousAddresses + match );
465 setModified( true );
466 cursorAtEnd();
468 break;
471 case KGlobalSettings::CompletionMan: // Short-Auto in fact
472 case KGlobalSettings::CompletionAuto:
474 //force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect
475 setCompletionMode( completionMode() );
477 if ( !m_searchString.isEmpty() ) {
479 //if only our \" is left, remove it since user has not typed it either
480 if ( m_searchExtended && m_searchString == "\"" ){
481 m_searchExtended = false;
482 m_searchString.clear();
483 setText( m_previousAddresses );
484 break;
487 QString match = s_completion->makeCompletion( m_searchString );
489 if ( !match.isEmpty() ) {
490 if ( match != m_searchString ) {
491 QString adds = m_previousAddresses + match;
492 setCompletedText( adds );
494 } else {
495 if ( !m_searchString.startsWith( '\"' ) ) {
496 //try with quoted text, if user has not type one already
497 match = s_completion->makeCompletion( "\"" + m_searchString );
498 if ( !match.isEmpty() && match != m_searchString ) {
499 m_searchString = "\"" + m_searchString;
500 m_searchExtended = true;
501 setText( m_previousAddresses + m_searchString );
502 setCompletedText( m_previousAddresses + match );
504 } else if ( m_searchExtended ) {
505 //our added \" does not work anymore, remove it
506 m_searchString = m_searchString.mid( 1 );
507 m_searchExtended = false;
508 setText( m_previousAddresses + m_searchString );
509 //now try again
510 match = s_completion->makeCompletion( m_searchString );
511 if ( !match.isEmpty() && match != m_searchString ) {
512 QString adds = m_previousAddresses + match;
513 setCompletedText( adds );
518 break;
521 case KGlobalSettings::CompletionNone:
522 default: // fall through
523 break;
527 void AddresseeLineEdit::slotPopupCompletion( const QString &completion )
529 setText( m_previousAddresses + completion.trimmed() );
530 cursorAtEnd();
531 // slotMatched( m_previousAddresses + completion );
532 updateSearchString();
535 void AddresseeLineEdit::slotReturnPressed( const QString &item )
537 Q_UNUSED( item );
538 if ( !completionBox()->selectedItems().isEmpty() ) {
539 slotPopupCompletion( completionBox()->selectedItems().first()->text() );
543 void AddresseeLineEdit::loadContacts()
545 s_completion->clear();
546 s_completionItemMap->clear();
547 s_addressesDirty = false;
548 //m_contactMap.clear();
550 QApplication::setOverrideCursor( QCursor( Qt::WaitCursor ) ); // loading might take a while
552 KConfig _config( "kpimcompletionorder" );
553 KConfigGroup config(&_config, "CompletionWeights" );
555 KABC::AddressBook *addressBook = KABC::StdAddressBook::self( true );
556 // Can't just use the addressbook's iterator, we need to know which subresource
557 // is behind which contact.
558 QList<KABC::Resource*> resources( addressBook->resources() );
559 QListIterator<KABC::Resource*> resit( resources );
560 while ( resit.hasNext() ) {
561 KABC::Resource *resource = resit.next();
562 KABC::ResourceABC *resabc = dynamic_cast<KABC::ResourceABC *>( resource );
563 if ( resabc ) { // IMAP KABC resource; need to associate each contact with the subresource
564 const QMap<QString, QString> uidToResourceMap = resabc->uidToResourceMap();
565 KABC::Resource::Iterator it;
566 for ( it = resource->begin(); it != resource->end(); ++it ) {
567 QString uid = (*it).uid();
568 QMap<QString, QString>::const_iterator wit = uidToResourceMap.find( uid );
569 const QString subresourceLabel = resabc->subresourceLabel( *wit );
570 int weight = ( wit != uidToResourceMap.end() ) ?
571 resabc->subresourceCompletionWeight( *wit ) : 80;
572 const int idx = addCompletionSource( subresourceLabel, weight );
573 addContact( *it, weight, idx );
575 } else { // KABC non-imap resource
576 int weight = config.readEntry( resource->identifier(), 60 );
577 int sourceIndex = addCompletionSource( resource->resourceName(), weight );
578 KABC::Resource::Iterator it;
579 for ( it = resource->begin(); it != resource->end(); ++it ) {
580 addContact( *it, weight, sourceIndex );
584 // add distribution list names
585 int weight = config.readEntry( resource->identifier(), 60 );
586 const QStringList distListNames = resource->allDistributionListNames();
587 foreach ( const QString &distList, distListNames ) {
588 //for CompletionAuto
589 addCompletionItem( distList, weight, s_completionSources->size() - 1 );
591 //for CompletionShell, CompletionPopup
592 QStringList sl( distList );
593 addCompletionItem( distList, weight, s_completionSources->size() - 1, &sl );
597 QApplication::restoreOverrideCursor();
599 if ( !m_addressBookConnected ) {
600 connect( addressBook, SIGNAL(addressBookChanged(AddressBook*)),
601 SLOT(loadContacts()) );
602 m_addressBookConnected = true;
606 void AddresseeLineEdit::addContact( const KABC::Addressee &addr, int weight, int source )
608 if ( KPIM::DistributionList::isDistributionList( addr ) ) {
610 //for CompletionAuto
611 addCompletionItem( addr.formattedName(), weight, source );
613 //for CompletionShell, CompletionPopup
614 QStringList sl( addr.formattedName() );
615 addCompletionItem( addr.formattedName(), weight, source, &sl );
617 return;
619 //m_contactMap.insert( addr.realName(), addr );
620 const QStringList emails = addr.emails();
621 QStringList::ConstIterator it;
622 const int prefEmailWeight = 1; //increment weight by prefEmailWeight
623 int isPrefEmail = prefEmailWeight; //first in list is preferredEmail
624 for ( it = emails.begin(); it != emails.end(); ++it ) {
625 //TODO: highlight preferredEmail
626 const QString email( (*it) );
627 const QString givenName = addr.givenName();
628 const QString familyName= addr.familyName();
629 const QString nickName = addr.nickName();
630 const QString domain = email.mid( email.indexOf( '@' ) + 1 );
631 QString fullEmail = addr.fullEmail( email );
632 //TODO: let user decide what fields to use in lookup, e.g. company, city, ...
634 //for CompletionAuto
635 if ( givenName.isEmpty() && familyName.isEmpty() ) {
636 addCompletionItem( fullEmail, weight + isPrefEmail, source ); // use whatever is there
637 } else {
638 const QString byFirstName= "\"" + givenName + " " + familyName + "\" <" + email + ">";
639 const QString byLastName = "\"" + familyName + ", " + givenName + "\" <" + email + ">";
640 addCompletionItem( byFirstName, weight + isPrefEmail, source );
641 addCompletionItem( byLastName, weight + isPrefEmail, source );
644 addCompletionItem( email, weight + isPrefEmail, source );
646 if ( !nickName.isEmpty() ){
647 const QString byNick = "\"" + nickName + "\" <" + email + ">";
648 addCompletionItem( byNick, weight + isPrefEmail, source );
651 if ( !domain.isEmpty() ){
652 const QString byDomain = '\"' + domain + ' ' +
653 familyName + ' ' + givenName +
654 "\" <" + email + '>';
655 addCompletionItem( byDomain, weight + isPrefEmail, source );
658 //for CompletionShell, CompletionPopup
659 QStringList keyWords;
660 const QString realName = addr.realName();
662 if ( !givenName.isEmpty() && !familyName.isEmpty() ) {
663 keyWords.append( givenName + ' ' + familyName );
664 keyWords.append( familyName + ' ' + givenName );
665 keyWords.append( familyName + ", " + givenName );
666 } else if ( !givenName.isEmpty() ) {
667 keyWords.append( givenName );
668 } else if ( !familyName.isEmpty() ) {
669 keyWords.append( familyName );
672 if ( !nickName.isEmpty() ) {
673 keyWords.append( nickName );
676 if ( !realName.isEmpty() ) {
677 keyWords.append( realName );
680 if ( !domain.isEmpty() ) {
681 keyWords.append( domain );
684 keyWords.append( email );
686 /* KMailCompletion does not have knowledge about identities, it stores emails and
687 * keywords for each email. KMailCompletion::allMatches does a lookup on the
688 * keywords and returns an ordered list of emails. In order to get the preferred
689 * email before others for each identity we use this little trick.
690 * We remove the <blank> in getAdjustedCompletionItems.
692 if ( isPrefEmail == prefEmailWeight ) {
693 fullEmail.replace( " <", " <" );
696 addCompletionItem( fullEmail, weight + isPrefEmail, source, &keyWords );
697 isPrefEmail = 0;
699 #if 0
700 int len = (*it).length();
701 if ( len == 0 ) {
702 continue;
704 if( '\0' == (*it)[len-1] ) {
705 --len;
707 const QString tmp = (*it).left( len );
708 const QString fullEmail = addr.fullEmail( tmp );
709 //kDebug(5300) <<"AddresseeLineEdit::addContact() \"" << fullEmail <<"\" weight=" << weight;
710 addCompletionItem( fullEmail.simplified(), weight, source );
711 // Try to guess the last name: if found, we add an extra
712 // entry to the list to make sure completion works even
713 // if the user starts by typing in the last name.
714 QString name( addr.realName().simplified() );
715 if ( name.endsWith( "\"" ) ) {
716 name.truncate( name.length()-1 );
718 if ( name.startsWith( "\"" ) ) {
719 name = name.mid( 1 );
722 // While we're here also add "email (full name)" for completion on the email
723 if ( !name.isEmpty() ) {
724 addCompletionItem( addr.preferredEmail() + " (" + name + ')', weight, source );
727 bool bDone = false;
728 int i = -1;
729 while ( ( i = name.lastIndexOf( ' ' ) ) > 1 && !bDone ) {
730 QString sLastName( name.mid( i + 1 ) );
731 // last names must be at least 2 chars long and must not end
732 // with a '.', like in "Jr." or "Sr."
733 if( !sLastName.isEmpty() &&
734 2 <= sLastName.length() &&
735 !sLastName.endsWith( "." ) ) {
736 name.truncate( i );
737 if ( !name.isEmpty() ){
738 sLastName.prepend( "\"" );
739 sLastName.append( ", " + name + "\" <" );
741 QString sExtraEntry( sLastName );
742 sExtraEntry.append( tmp.isEmpty() ? addr.preferredEmail() : tmp );
743 sExtraEntry.append( ">" );
744 addCompletionItem( sExtraEntry.simplified(), weight, source );
745 bDone = true;
747 if ( !bDone ) {
748 name.truncate( i );
749 if ( name.endsWith( "\"" ) ) {
750 name.truncate( name.length() - 1 );
754 #endif
758 void AddresseeLineEdit::addCompletionItem( const QString &string, int weight,
759 int completionItemSource,
760 const QStringList *keyWords )
762 // Check if there is an exact match for item already, and use the
763 // maximum weight if so. Since there's no way to get the information
764 // from KCompletion, we have to keep our own QMap.
766 CompletionItemsMap::iterator it = s_completionItemMap->find( string );
767 if ( it != s_completionItemMap->end() ) {
768 weight = qMax( ( *it ).first, weight );
769 ( *it ).first = weight;
770 } else {
771 s_completionItemMap->insert( string, qMakePair( weight, completionItemSource ) );
773 if ( keyWords == 0 ) {
774 s_completion->addItem( string, weight );
775 } else {
776 s_completion->addItemWithKeys( string, weight, keyWords );
780 void AddresseeLineEdit::slotStartLDAPLookup()
782 KGlobalSettings::Completion mode = completionMode();
784 if ( mode == KGlobalSettings::CompletionNone ) {
785 return;
788 if ( !s_LDAPSearch->isAvailable() ) {
789 return;
791 if ( s_LDAPLineEdit != this ) {
792 return;
795 startLoadingLDAPEntries();
798 void AddresseeLineEdit::stopLDAPLookup()
800 s_LDAPSearch->cancelSearch();
801 s_LDAPLineEdit = 0;
804 void AddresseeLineEdit::startLoadingLDAPEntries()
806 QString s( *s_LDAPText );
807 // TODO cache last?
808 QString prevAddr;
809 int n = s.lastIndexOf( ',' );
810 if ( n >= 0 ) {
811 prevAddr = s.left( n + 1 ) + ' ';
812 s = s.mid( n + 1, 255 ).trimmed();
815 if ( s.isEmpty() ) {
816 return;
819 //loadContacts(); // TODO reuse these?
820 s_LDAPSearch->startSearch( s );
823 void AddresseeLineEdit::slotLDAPSearchData( const KPIM::LdapResultList &adrs )
825 if ( adrs.isEmpty() || s_LDAPLineEdit != this ) {
826 return;
829 for ( KPIM::LdapResultList::ConstIterator it = adrs.begin(); it != adrs.end(); ++it ) {
830 KABC::Addressee addr;
831 addr.setNameFromString( (*it).name );
832 addr.setEmails( (*it).email );
834 if ( !s_ldapClientToCompletionSourceMap->contains( (*it).clientNumber ) )
835 updateLDAPWeights(); // we got results from a new source, so update the completion sources
837 addContact( addr, (*it).completionWeight, (*s_ldapClientToCompletionSourceMap)[ (*it ).clientNumber ] );
839 if ( ( hasFocus() || completionBox()->hasFocus() ) &&
840 completionMode() != KGlobalSettings::CompletionNone &&
841 completionMode() != KGlobalSettings::CompletionShell ) {
842 setText( m_previousAddresses + m_searchString );
843 // only complete again if the user didn't change the selection while
844 // we were waiting; otherwise the completion box will be closed
845 QListWidgetItem *current = completionBox()->currentItem();
846 if ( !current || m_searchString.trimmed() != current->text().trimmed() ) {
847 doCompletion( m_lastSearchMode );
852 void AddresseeLineEdit::setCompletedItems( const QStringList &items, bool autoSuggest )
854 KCompletionBox *completionBox = this->completionBox();
856 if ( !items.isEmpty() &&
857 !( items.count() == 1 && m_searchString == items.first() ) ) {
858 completionBox->setItems( items );
860 if ( !completionBox->isVisible() ) {
861 if ( !m_searchString.isEmpty() ) {
862 completionBox->setCancelledText( m_searchString );
864 completionBox->popup();
865 // we have to install the event filter after popup(), since that
866 // calls show(), and that's where KCompletionBox installs its filter.
867 // We want to be first, though, so do it now.
868 if ( s_completion->order() == KCompletion::Weighted ) {
869 qApp->installEventFilter( this );
873 QListWidgetItem *item = completionBox->item( 1 );
874 if ( item ) {
875 completionBox->blockSignals( true );
876 completionBox->setCurrentItem( item );
877 item->setSelected( true );
878 completionBox->blockSignals( false );
881 if ( autoSuggest ) {
882 int index = items.first().indexOf( m_searchString );
883 QString newText = items.first().mid( index );
884 setUserSelection( false );
885 setCompletedText( newText, true );
887 } else {
888 if ( completionBox && completionBox->isVisible() ) {
889 completionBox->hide();
890 completionBox->setItems( QStringList() );
895 void AddresseeLineEdit::contextMenuEvent( QContextMenuEvent *e )
897 QMenu *menu = createStandardContextMenu();
898 menu->exec( e->globalPos() );
899 delete menu;
902 QMenu *AddresseeLineEdit::createStandardContextMenu()
904 //disable modes not supported by KMailCompletion
905 setCompletionModeDisabled( KGlobalSettings::CompletionMan );
906 setCompletionModeDisabled( KGlobalSettings::CompletionPopupAuto );
907 QMenu *menu = KLineEdit::createStandardContextMenu();
908 if ( !menu ) {
909 return 0;
912 if ( m_useCompletion ) {
913 menu->addAction( i18n( "Configure Completion Order..." ),
914 this, SLOT(slotEditCompletionOrder()) );
916 return menu;
919 void AddresseeLineEdit::slotEditCompletionOrder()
921 init(); // for s_LDAPSearch
922 CompletionOrderEditor editor( s_LDAPSearch, this );
923 editor.exec();
924 if ( m_useCompletion ) {
925 updateLDAPWeights();
926 s_addressesDirty = true;
930 void KPIM::AddresseeLineEdit::slotIMAPCompletionOrderChanged()
932 if ( m_useCompletion ) {
933 s_addressesDirty = true;
937 void KPIM::AddresseeLineEdit::slotUserCancelled( const QString &cancelText )
939 if ( s_LDAPSearch && s_LDAPLineEdit == this ) {
940 stopLDAPLookup();
942 userCancelled( m_previousAddresses + cancelText ); // in KLineEdit
945 void AddresseeLineEdit::updateSearchString()
947 m_searchString = text();
949 int n = -1;
950 bool inQuote = false;
951 for ( uint i = 0, searchStringLength = m_searchString.length(); i < searchStringLength; ++i ) {
952 if ( m_searchString[ i ] == '"' ) {
953 inQuote = !inQuote;
955 if ( m_searchString[ i ] == '\\' &&
956 ( i + 1 ) < searchStringLength && m_searchString[ i + 1 ] == '"' ) {
957 ++i;
959 if ( inQuote ) {
960 continue;
962 if ( m_searchString[ i ] == ',' || ( m_useSemiColonAsSeparator && m_searchString[ i ] == ';' ) ) {
963 n = i;
967 if ( n >= 0 ) {
968 ++n; // Go past the ","
970 int len = m_searchString.length();
972 // Increment past any whitespace...
973 while ( n < len && m_searchString[ n ].isSpace() ) {
974 ++n;
977 m_previousAddresses = m_searchString.left( n );
978 m_searchString = m_searchString.mid( n ).trimmed();
979 } else {
980 m_previousAddresses.clear();
984 void KPIM::AddresseeLineEdit::slotCompletion()
986 // Called by KLineEdit's keyPressEvent for CompletionModes
987 // Auto,Popup -> new text, update search string.
988 // not called for CompletionShell, this is been taken care of
989 // in AddresseeLineEdit::keyPressEvent
991 updateSearchString();
992 if ( completionBox() ) {
993 completionBox()->setCancelledText( m_searchString );
995 doCompletion( false );
998 // not cached, to make sure we get an up-to-date value when it changes
999 KCompletion::CompOrder KPIM::AddresseeLineEdit::completionOrder()
1001 KConfig _config( "kpimcompletionorder" );
1002 KConfigGroup config( &_config, "General" );
1003 const QString order = config.readEntry( "CompletionOrder", "Weighted" );
1005 if ( order == "Weighted" ) {
1006 return KCompletion::Weighted;
1007 } else {
1008 return KCompletion::Sorted;
1012 int KPIM::AddresseeLineEdit::addCompletionSource( const QString &source, int weight )
1014 QMap<QString,int>::iterator it = s_completionSourceWeights->find( source );
1015 if ( it == s_completionSourceWeights->end() )
1016 s_completionSourceWeights->insert( source, weight );
1017 else
1018 (*s_completionSourceWeights)[source] = weight;
1020 int sourceIndex = s_completionSources->indexOf( source );
1021 if ( sourceIndex == -1 ) {
1022 s_completionSources->append( source );
1023 return s_completionSources->size() - 1;
1025 else
1026 return sourceIndex;
1029 bool KPIM::AddresseeLineEdit::eventFilter( QObject *obj, QEvent *e )
1031 if ( m_completionInitialized &&
1032 ( obj == completionBox() ||
1033 completionBox()->findChild<QWidget*>( obj->objectName() ) == obj ) ) {
1034 if ( e->type() == QEvent::MouseButtonPress ||
1035 e->type() == QEvent::MouseMove ||
1036 e->type() == QEvent::MouseButtonRelease ||
1037 e->type() == QEvent::MouseButtonDblClick ) {
1038 QMouseEvent* me = static_cast<QMouseEvent*>( e );
1039 // find list box item at the event position
1040 QListWidgetItem *item = completionBox()->itemAt( me->pos() );
1041 if ( !item ) {
1042 // In the case of a mouse move outside of the box we don't want
1043 // the parent to fuzzy select a header by mistake.
1044 bool eat = e->type() == QEvent::MouseMove;
1045 return eat;
1047 // avoid selection of headers on button press, or move or release while
1048 // a button is pressed
1049 Qt::MouseButtons btns = me->buttons();
1050 if ( e->type() == QEvent::MouseButtonPress ||
1051 e->type() == QEvent::MouseButtonDblClick ||
1052 btns & Qt::LeftButton || btns & Qt::MidButton ||
1053 btns & Qt::RightButton ) {
1054 if ( itemIsHeader( item ) ) {
1055 return true; // eat the event, we don't want anything to happen
1056 } else {
1057 // if we are not on one of the group heading, make sure the item
1058 // below or above is selected, not the heading, inadvertedly, due
1059 // to fuzzy auto-selection from QListBox
1060 completionBox()->setCurrentItem( item );
1061 item->setSelected( true );
1062 if ( e->type() == QEvent::MouseMove ) {
1063 return true; // avoid fuzzy selection behavior
1069 if ( ( obj == this ) &&
1070 ( e->type() == QEvent::ShortcutOverride ) ) {
1071 QKeyEvent *ke = static_cast<QKeyEvent*>( e );
1072 if ( ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down || ke->key() == Qt::Key_Tab ) {
1073 ke->accept();
1074 return true;
1077 if ( ( obj == this ) &&
1078 ( e->type() == QEvent::KeyPress ) &&
1079 completionBox()->isVisible() ) {
1080 QKeyEvent *ke = static_cast<QKeyEvent*>( e );
1081 int currentIndex = completionBox()->currentRow();
1082 if ( ke->key() == Qt::Key_Up ) {
1083 //kDebug() <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex;
1084 // figure out if the item we would be moving to is one we want
1085 // to ignore. If so, go one further
1086 QListWidgetItem *itemAbove = completionBox()->item( currentIndex - 1 );
1087 if ( itemAbove && itemIsHeader(itemAbove) ) {
1088 // there is a header above is, check if there is even further up
1089 // and if so go one up, so it'll be selected
1090 if ( currentIndex > 1 && completionBox()->item( currentIndex - 2 ) ) {
1091 //kDebug() <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1;
1092 completionBox()->setCurrentRow( currentIndex -2 );
1093 completionBox()->item( currentIndex - 2 )->setSelected( true );
1094 } else if ( currentIndex == 1 ) {
1095 // nothing to skip to, let's stay where we are, but make sure the
1096 // first header becomes visible, if we are the first real entry
1097 completionBox()->scrollToItem( completionBox()->item( 0 ) );
1098 QListWidgetItem *i = completionBox()->item( currentIndex );
1099 if ( i ) {
1100 completionBox()->setCurrentItem( i );
1101 i->setSelected( true );
1104 return true;
1106 } else if ( ke->key() == Qt::Key_Down ) {
1107 // same strategy for downwards
1108 //kDebug() <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex;
1109 QListWidgetItem *itemBelow = completionBox()->item( currentIndex + 1 );
1110 if ( itemBelow && itemIsHeader( itemBelow ) ) {
1111 if ( completionBox()->item( currentIndex + 2 ) ) {
1112 //kDebug() <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1;
1113 completionBox()->setCurrentRow( currentIndex + 2 );
1114 completionBox()->item( currentIndex + 2 )->setSelected( true );
1115 } else {
1116 // nothing to skip to, let's stay where we are
1117 QListWidgetItem *i = completionBox()->item( currentIndex );
1118 if ( i ) {
1119 completionBox()->setCurrentItem( i );
1120 i->setSelected( true );
1123 return true;
1125 // special case of the initial selection, which is unfortunately a header.
1126 // Setting it to selected tricks KCompletionBox into not treating is special
1127 // and selecting making it current, instead of the one below.
1128 QListWidgetItem *item = completionBox()->item( currentIndex );
1129 if ( item && itemIsHeader(item) ) {
1130 completionBox()->setCurrentItem( item );
1131 item->setSelected( true );
1133 } else if ( ke->key() == Qt::Key_Tab || ke->key() == Qt::Key_Backtab ) {
1134 /// first, find the header of the current section
1135 QListWidgetItem *myHeader = 0;
1136 int i = currentIndex;
1137 while ( i>=0 ) {
1138 if ( itemIsHeader( completionBox()->item(i) ) ) {
1139 myHeader = completionBox()->item( i );
1140 break;
1142 i--;
1144 Q_ASSERT( myHeader ); // we should always be able to find a header
1146 // find the next header (searching backwards, for Qt::Key_Backtab
1147 QListWidgetItem *nextHeader = 0;
1148 const int iterationstep = ke->key() == Qt::Key_Tab ? 1 : -1;
1149 // when iterating forward, start at the currentindex, when backwards,
1150 // one up from our header, or at the end
1151 uint j = ke->key() == Qt::Key_Tab ?
1152 currentIndex : i == 0 ?
1153 completionBox()->count() - 1 : ( i - 1 ) % completionBox()->count();
1154 while ( ( nextHeader = completionBox()->item( j ) ) && nextHeader != myHeader ) {
1155 if ( itemIsHeader(nextHeader) ) {
1156 break;
1158 j = ( j + iterationstep ) % completionBox()->count();
1160 if ( nextHeader && nextHeader != myHeader ) {
1161 QListWidgetItem *item = completionBox()->item( j + 1 );
1162 if ( item && !itemIsHeader(item) ) {
1163 completionBox()->setCurrentItem( item );
1164 item->setSelected( true );
1167 return true;
1170 return KLineEdit::eventFilter( obj, e );
1173 class SourceWithWeight {
1174 public:
1175 int weight; // the weight of the source
1176 QString sourceName; // the name of the source, e.g. "LDAP Server"
1177 int index; // index into s_completionSources
1179 bool operator< ( const SourceWithWeight &other ) const {
1180 if ( weight > other.weight )
1181 return true;
1182 if ( weight < other.weight )
1183 return false;
1184 return sourceName < other.sourceName;
1188 const QStringList KPIM::AddresseeLineEdit::getAdjustedCompletionItems( bool fullSearch )
1190 QStringList items = fullSearch ?
1191 s_completion->allMatches( m_searchString )
1192 : s_completion->substringCompletion( m_searchString );
1194 //force items to be sorted by email
1195 items.sort();
1197 // For weighted mode, the algorithm is the following:
1198 // In the first loop, we add each item to its section (there is one section per completion source)
1199 // We also add spaces in front of the items.
1200 // The sections are appended to the items list.
1201 // In the second loop, we then walk through the sections and add all the items in there to the
1202 // sorted item list, which is the final result.
1204 // The algo for non-weighted mode is different.
1206 int lastSourceIndex = -1;
1207 unsigned int i = 0;
1209 // Maps indices of the items list, which are section headers/source items,
1210 // to a QStringList which are the items of that section/source.
1211 QMap<int, QStringList> sections;
1212 QStringList sortedItems;
1213 for ( QStringList::Iterator it = items.begin(); it != items.end(); ++it, ++i ) {
1214 CompletionItemsMap::const_iterator cit = s_completionItemMap->constFind(*it);
1215 if ( cit == s_completionItemMap->constEnd() ) {
1216 continue;
1218 int idx = (*cit).second;
1220 if ( s_completion->order() == KCompletion::Weighted ) {
1221 if ( lastSourceIndex == -1 || lastSourceIndex != idx ) {
1222 const QString sourceLabel( (*s_completionSources)[idx] );
1223 if ( sections.find(idx) == sections.end() ) {
1224 it = items.insert( it, sourceLabel );
1225 ++it; //skip new item
1227 lastSourceIndex = idx;
1229 (*it) = (*it).prepend( s_completionItemIndentString );
1230 // remove preferred email sort <blank> added in addContact()
1231 (*it).replace( " <", " <" );
1233 sections[idx].append( *it );
1235 if ( s_completion->order() == KCompletion::Sorted ) {
1236 sortedItems.append( *it );
1240 if ( s_completion->order() == KCompletion::Weighted ) {
1242 // Sort the sections
1243 QList<SourceWithWeight> sourcesAndWeights;
1244 for ( int i = 0; i < s_completionSources->size(); i++ ) {
1245 SourceWithWeight sww;
1246 sww.sourceName = (*s_completionSources)[i];
1247 sww.weight = (*s_completionSourceWeights)[sww.sourceName];
1248 sww.index = i;
1249 sourcesAndWeights.append( sww );
1251 qSort( sourcesAndWeights.begin(), sourcesAndWeights.end() );
1253 // Add the sections and their items to the final sortedItems result list
1254 for( int i = 0; i < sourcesAndWeights.size(); i++ ) {
1255 QStringList sectionItems = sections[sourcesAndWeights[i].index];
1256 if ( !sectionItems.isEmpty() ) {
1257 sortedItems.append( sourcesAndWeights[i].sourceName );
1258 QStringList sectionItems = sections[sourcesAndWeights[i].index];
1259 foreach( const QString &itemInSection, sectionItems ) {
1260 sortedItems.append( itemInSection );
1264 } else {
1265 sortedItems.sort();
1267 return sortedItems;
1269 #include "addresseelineedit.moc"