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>
40 #include <KStandardDirs>
41 #include <KStandardShortcut>
43 #include <K3StaticDeleter>
45 #include <QApplication>
53 #include <QMouseEvent>
55 #include <QtDBus/QDBusConnection>
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" );
93 name
+= QByteArray().setNum( s_count
);
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;
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();
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
);
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 ) {
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
)
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 );
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 );
231 const QString oldContent
= text();
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() )
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 ) {
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
);
269 QString newText
= t
.trimmed();
270 if ( newText
.isEmpty() ) {
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:" ) ) ) {
284 newText
= url
.path();
285 } else if ( newText
.indexOf( " at " ) != -1 ) {
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();
295 int pos
= cursorPosition( );
297 if ( hasSelectedText() ) {
298 // Cut away the selection.
299 start_sel
= selectionStart();
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() ) {
310 } else if ( pos
>= eot
) {
311 if ( contents
[ eot
- 1 ] == ',' ) {
314 contents
.truncate( eot
);
319 contents
= contents
.left( pos
) + newText
+ contents
.mid( pos
);
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
) {
337 m_smartPaste
= false;
340 void AddresseeLineEdit::mouseReleaseEvent( QMouseEvent
*e
)
342 // reimplemented from QLineEdit::mouseReleaseEvent()
343 if ( m_useCompletion
&&
344 QApplication::clipboard()->supportsSelection() &&
346 e
->button() == Qt::MidButton
) {
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() ) {
367 } else if ( contents
[ eot
- 1 ] == ',' ) {
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
) {
375 if ( u
.protocol() == "mailto" ) {
378 address
= KUrl::fromPercentEncoding( u
.path().toLatin1() );
379 address
= KMime::decodeRFC2047String( address
.toAscii() );
380 if ( !contents
.isEmpty() ) {
381 contents
.append( ", " );
383 contents
.append( address
);
394 if ( m_useCompletion
) {
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
) {
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?
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 );
440 setCompletionMode( mode
); //set back to previous mode
445 case KGlobalSettings::CompletionPopupAuto
:
447 if ( m_searchString
.isEmpty() ) {
450 //else: fall-through to the CompletionPopup case
453 case KGlobalSettings::CompletionPopup
:
455 const QStringList items
= getAdjustedCompletionItems( true );
456 setCompletedItems( items
, false );
460 case KGlobalSettings::CompletionShell
:
462 QString match
= s_completion
->makeCompletion( m_searchString
);
463 if ( !match
.isNull() && match
!= m_searchString
) {
464 setText( m_previousAddresses
+ match
);
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
);
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
);
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
);
510 match
= s_completion
->makeCompletion( m_searchString
);
511 if ( !match
.isEmpty() && match
!= m_searchString
) {
512 QString adds
= m_previousAddresses
+ match
;
513 setCompletedText( adds
);
521 case KGlobalSettings::CompletionNone
:
522 default: // fall through
527 void AddresseeLineEdit::slotPopupCompletion( const QString
&completion
)
529 setText( m_previousAddresses
+ completion
.trimmed() );
531 // slotMatched( m_previousAddresses + completion );
532 updateSearchString();
535 void AddresseeLineEdit::slotReturnPressed( const QString
&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
) {
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
) ) {
611 addCompletionItem( addr
.formattedName(), weight
, source
);
613 //for CompletionShell, CompletionPopup
614 QStringList
sl( addr
.formattedName() );
615 addCompletionItem( addr
.formattedName(), weight
, source
, &sl
);
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, ...
635 if ( givenName
.isEmpty() && familyName
.isEmpty() ) {
636 addCompletionItem( fullEmail
, weight
+ isPrefEmail
, source
); // use whatever is there
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
);
700 int len
= (*it
).length();
704 if( '\0' == (*it
)[len
-1] ) {
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
);
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( "." ) ) {
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
);
749 if ( name
.endsWith( "\"" ) ) {
750 name
.truncate( name
.length() - 1 );
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
;
771 s_completionItemMap
->insert( string
, qMakePair( weight
, completionItemSource
) );
773 if ( keyWords
== 0 ) {
774 s_completion
->addItem( string
, weight
);
776 s_completion
->addItemWithKeys( string
, weight
, keyWords
);
780 void AddresseeLineEdit::slotStartLDAPLookup()
782 KGlobalSettings::Completion mode
= completionMode();
784 if ( mode
== KGlobalSettings::CompletionNone
) {
788 if ( !s_LDAPSearch
->isAvailable() ) {
791 if ( s_LDAPLineEdit
!= this ) {
795 startLoadingLDAPEntries();
798 void AddresseeLineEdit::stopLDAPLookup()
800 s_LDAPSearch
->cancelSearch();
804 void AddresseeLineEdit::startLoadingLDAPEntries()
806 QString
s( *s_LDAPText
);
809 int n
= s
.lastIndexOf( ',' );
811 prevAddr
= s
.left( n
+ 1 ) + ' ';
812 s
= s
.mid( n
+ 1, 255 ).trimmed();
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 ) {
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 );
875 completionBox
->blockSignals( true );
876 completionBox
->setCurrentItem( item
);
877 item
->setSelected( true );
878 completionBox
->blockSignals( false );
882 int index
= items
.first().indexOf( m_searchString
);
883 QString newText
= items
.first().mid( index
);
884 setUserSelection( false );
885 setCompletedText( newText
, true );
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() );
902 QMenu
*AddresseeLineEdit::createStandardContextMenu()
904 //disable modes not supported by KMailCompletion
905 setCompletionModeDisabled( KGlobalSettings::CompletionMan
);
906 setCompletionModeDisabled( KGlobalSettings::CompletionPopupAuto
);
907 QMenu
*menu
= KLineEdit::createStandardContextMenu();
912 if ( m_useCompletion
) {
913 menu
->addAction( i18n( "Configure Completion Order..." ),
914 this, SLOT(slotEditCompletionOrder()) );
919 void AddresseeLineEdit::slotEditCompletionOrder()
921 init(); // for s_LDAPSearch
922 CompletionOrderEditor
editor( s_LDAPSearch
, this );
924 if ( m_useCompletion
) {
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 ) {
942 userCancelled( m_previousAddresses
+ cancelText
); // in KLineEdit
945 void AddresseeLineEdit::updateSearchString()
947 m_searchString
= text();
950 bool inQuote
= false;
951 for ( uint i
= 0, searchStringLength
= m_searchString
.length(); i
< searchStringLength
; ++i
) {
952 if ( m_searchString
[ i
] == '"' ) {
955 if ( m_searchString
[ i
] == '\\' &&
956 ( i
+ 1 ) < searchStringLength
&& m_searchString
[ i
+ 1 ] == '"' ) {
962 if ( m_searchString
[ i
] == ',' || ( m_useSemiColonAsSeparator
&& m_searchString
[ i
] == ';' ) ) {
968 ++n
; // Go past the ","
970 int len
= m_searchString
.length();
972 // Increment past any whitespace...
973 while ( n
< len
&& m_searchString
[ n
].isSpace() ) {
977 m_previousAddresses
= m_searchString
.left( n
);
978 m_searchString
= m_searchString
.mid( n
).trimmed();
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
;
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
);
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;
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() );
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
;
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
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
) {
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
);
1100 completionBox()->setCurrentItem( i
);
1101 i
->setSelected( 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 );
1116 // nothing to skip to, let's stay where we are
1117 QListWidgetItem
*i
= completionBox()->item( currentIndex
);
1119 completionBox()->setCurrentItem( i
);
1120 i
->setSelected( 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
;
1138 if ( itemIsHeader( completionBox()->item(i
) ) ) {
1139 myHeader
= completionBox()->item( 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
) ) {
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 );
1170 return KLineEdit::eventFilter( obj
, e
);
1173 class SourceWithWeight
{
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
)
1182 if ( weight
< other
.weight
)
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
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;
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() ) {
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
];
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
);
1269 #include "addresseelineedit.moc"