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"
30 #include "completionordereditor.h"
33 #include <Akonadi/Contact/ContactSearchJob>
34 #include <Akonadi/Contact/ContactGroupSearchJob>
35 #include <Akonadi/CollectionFetchJob>
36 #include <Akonadi/EntityDisplayAttribute>
37 #include <Akonadi/ItemFetchScope>
39 #include <KPIMUtils/Email>
41 #include <KLDAP/LdapServer>
45 #include <KConfigGroup>
46 #include <KCompletionBox>
49 #include <KStandardDirs>
50 #include <KStandardShortcut>
53 #include <solid/networking.h>
55 #include <QApplication>
63 #include <QMouseEvent>
66 #include <QtDBus/QDBusConnection>
70 class AddresseeLineEditStatic
74 AddresseeLineEditStatic()
75 : completion( new KMailCompletion
),
82 ~AddresseeLineEditStatic()
89 KMailCompletion
*completion
;
90 KPIM::CompletionItemsMap completionItemMap
;
91 QStringList completionSources
;
93 KLDAP::LdapClientSearch
*ldapSearch
;
95 AddresseeLineEdit
*ldapLineEdit
;
96 // The weights associated with the completion sources in s_static->completionSources.
97 // Both are maintained by addCompletionSource(), don't attempt to modifiy those yourself.
98 QMap
<QString
, int> completionSourceWeights
;
99 // maps LDAP client indices to completion source indices
100 // the assumption that they are always the first n indices in s_static->completion
101 // does not hold when clients are added later on
102 QMap
<int, int> ldapClientToCompletionSourceMap
;
103 // holds the cached mapping from akonadi collection id to the completion source index
104 QMap
<Akonadi::Collection::Id
, int> akonadiCollectionToCompletionSourceMap
;
105 // a list of akonadi items (contacts) that have not had their collection fetched yet
106 Akonadi::Item::List akonadiPendingItems
;
109 K_GLOBAL_STATIC( AddresseeLineEditStatic
, s_static
)
111 // needs to be unique, but the actual name doesn't matter much
112 static QByteArray
newLineEditObjectName()
114 static int s_count
= 0;
115 QByteArray
name( "KPIM::AddresseeLineEdit" );
118 name
+= QByteArray().setNum( s_count
);
123 static const QString s_completionItemIndentString
= " ";
125 static bool itemIsHeader( const QListWidgetItem
*item
)
127 return item
&& !item
->text().startsWith( s_completionItemIndentString
);
130 class SourceWithWeight
133 int weight
; // the weight of the source
134 QString sourceName
; // the name of the source, e.g. "LDAP Server"
135 int index
; // index into s_static->completionSources
137 bool operator< ( const SourceWithWeight
&other
) const
139 if ( weight
> other
.weight
) {
143 if ( weight
< other
.weight
) {
147 return sourceName
< other
.sourceName
;
151 class AddresseeLineEdit::Private
154 Private( AddresseeLineEdit
*qq
, bool enableCompletion
)
156 m_useCompletion( enableCompletion
),
157 m_completionInitialized( false ),
158 m_smartPaste( false ),
159 m_addressBookConnected( false ),
160 m_searchExtended( false ),
161 m_useSemicolonAsSeparator( false )
166 void startLoadingLDAPEntries();
167 void stopLDAPLookup();
168 void updateLDAPWeights();
169 void setCompletedItems( const QStringList
&items
, bool autoSuggest
);
170 void addCompletionItem( const QString
&string
, int weight
, int source
,
171 const QStringList
*keyWords
= 0 );
172 const QStringList
adjustedCompletionItems( bool fullSearch
);
173 void updateSearchString();
174 void akonadiPerformSearch();
175 void akonadiHandlePending();
176 void doCompletion( bool ctrlT
);
178 void slotCompletion();
179 void slotPopupCompletion( const QString
& );
180 void slotReturnPressed( const QString
& );
181 void slotStartLDAPLookup();
182 void slotLDAPSearchData( const KLDAP::LdapResult::List
& );
183 void slotEditCompletionOrder();
184 void slotUserCancelled( const QString
& );
185 void slotAkonadiSearchResult( KJob
* );
186 void slotAkonadiCollectionsReceived( const Akonadi::Collection::List
& );
188 static KCompletion::CompOrder
completionOrder();
190 AddresseeLineEdit
*q
;
191 QString m_previousAddresses
;
192 QString m_searchString
;
193 bool m_useCompletion
;
194 bool m_completionInitialized
;
196 bool m_addressBookConnected
;
197 bool m_lastSearchMode
;
198 bool m_searchExtended
; //has \" been added?
199 bool m_useSemicolonAsSeparator
;
202 void AddresseeLineEdit::Private::init()
204 if ( !s_static
.exists() ) {
205 s_static
->completion
->setOrder( completionOrder() );
206 s_static
->completion
->setIgnoreCase( true );
209 if ( m_useCompletion
) {
210 if ( !s_static
->ldapTimer
) {
211 s_static
->ldapTimer
= new QTimer
;
212 s_static
->ldapSearch
= new KLDAP::LdapClientSearch
;
217 if ( !m_completionInitialized
) {
218 q
->setCompletionObject( s_static
->completion
, false );
219 q
->connect( q
, SIGNAL(completion(QString
)),
220 q
, SLOT(slotCompletion()) );
221 q
->connect( q
, SIGNAL(returnPressed(QString
)),
222 q
, SLOT(slotReturnPressed(QString
)) );
224 KCompletionBox
*box
= q
->completionBox();
225 q
->connect( box
, SIGNAL(activated(QString
)),
226 q
, SLOT(slotPopupCompletion(QString
)) );
227 q
->connect( box
, SIGNAL(userCancelled(QString
)),
228 q
, SLOT(slotUserCancelled(QString
)) );
230 q
->connect( s_static
->ldapTimer
, SIGNAL(timeout()), SLOT(slotStartLDAPLookup()) );
231 q
->connect( s_static
->ldapSearch
, SIGNAL(searchData(KLDAP::LdapResult::List
)),
232 SLOT(slotLDAPSearchData(KLDAP::LdapResult::List
)) );
234 m_completionInitialized
= true;
239 void AddresseeLineEdit::Private::startLoadingLDAPEntries()
241 QString
text( s_static
->ldapText
);
245 const int index
= text
.lastIndexOf( QLatin1Char( ',' ) );
247 prevAddr
= text
.left( index
+ 1 ) + QLatin1Char( ' ' );
248 text
= text
.mid( index
+ 1, 255 ).trimmed();
251 if ( text
.isEmpty() ) {
255 s_static
->ldapSearch
->startSearch( text
);
258 void AddresseeLineEdit::Private::stopLDAPLookup()
260 s_static
->ldapSearch
->cancelSearch();
261 s_static
->ldapLineEdit
= 0;
264 void AddresseeLineEdit::Private::updateLDAPWeights()
266 /* Add completion sources for all ldap server, 0 to n. Added first so
267 * that they map to the LdapClient::clientNumber() */
268 s_static
->ldapSearch
->updateCompletionWeights();
270 foreach ( const KLDAP::LdapClient
*client
, s_static
->ldapSearch
->clients() ) {
271 const int sourceIndex
=
272 q
->addCompletionSource( QLatin1String( "LDAP server: " ) + client
->server().host(),
273 client
->completionWeight() );
275 s_static
->ldapClientToCompletionSourceMap
.insert( clientIndex
, sourceIndex
);
281 void AddresseeLineEdit::Private::setCompletedItems( const QStringList
&items
, bool autoSuggest
)
283 KCompletionBox
*completionBox
= q
->completionBox();
285 if ( !items
.isEmpty() &&
286 !( items
.count() == 1 && m_searchString
== items
.first() ) ) {
288 completionBox
->setItems( items
);
290 if ( !completionBox
->isVisible() ) {
291 if ( !m_searchString
.isEmpty() ) {
292 completionBox
->setCancelledText( m_searchString
);
294 completionBox
->popup();
295 // we have to install the event filter after popup(), since that
296 // calls show(), and that's where KCompletionBox installs its filter.
297 // We want to be first, though, so do it now.
298 if ( s_static
->completion
->order() == KCompletion::Weighted
) {
299 qApp
->installEventFilter( q
);
303 QListWidgetItem
*item
= completionBox
->item( 1 );
305 completionBox
->blockSignals( true );
306 completionBox
->setCurrentItem( item
);
307 item
->setSelected( true );
308 completionBox
->blockSignals( false );
312 const int index
= items
.first().indexOf( m_searchString
);
313 const QString newText
= items
.first().mid( index
);
314 q
->setUserSelection( false );
315 q
->setCompletedText( newText
, true );
318 if ( completionBox
&& completionBox
->isVisible() ) {
319 completionBox
->hide();
320 completionBox
->setItems( QStringList() );
325 void AddresseeLineEdit::Private::addCompletionItem( const QString
&string
, int weight
,
326 int completionItemSource
,
327 const QStringList
*keyWords
)
329 // Check if there is an exact match for item already, and use the
330 // maximum weight if so. Since there's no way to get the information
331 // from KCompletion, we have to keep our own QMap.
333 CompletionItemsMap::iterator it
= s_static
->completionItemMap
.find( string
);
334 if ( it
!= s_static
->completionItemMap
.end() ) {
335 weight
= qMax( ( *it
).first
, weight
);
336 ( *it
).first
= weight
;
338 s_static
->completionItemMap
.insert( string
, qMakePair( weight
, completionItemSource
) );
341 if ( keyWords
== 0 ) {
342 s_static
->completion
->addItem( string
, weight
);
344 s_static
->completion
->addItemWithKeys( string
, weight
, keyWords
);
348 const QStringList
KPIM::AddresseeLineEdit::Private::adjustedCompletionItems( bool fullSearch
)
350 QStringList items
= fullSearch
?
351 s_static
->completion
->allMatches( m_searchString
) :
352 s_static
->completion
->substringCompletion( m_searchString
);
354 //force items to be sorted by email
357 // For weighted mode, the algorithm is the following:
358 // In the first loop, we add each item to its section (there is one section per completion source)
359 // We also add spaces in front of the items.
360 // The sections are appended to the items list.
361 // In the second loop, we then walk through the sections and add all the items in there to the
362 // sorted item list, which is the final result.
364 // The algo for non-weighted mode is different.
366 int lastSourceIndex
= -1;
369 // Maps indices of the items list, which are section headers/source items,
370 // to a QStringList which are the items of that section/source.
371 QMap
<int, QStringList
> sections
;
372 QStringList sortedItems
;
373 for ( QStringList::Iterator it
= items
.begin(); it
!= items
.end(); ++it
, ++i
) {
374 CompletionItemsMap::const_iterator cit
= s_static
->completionItemMap
.constFind( *it
);
375 if ( cit
== s_static
->completionItemMap
.constEnd() ) {
379 const int index
= (*cit
).second
;
381 if ( s_static
->completion
->order() == KCompletion::Weighted
) {
382 if ( lastSourceIndex
== -1 || lastSourceIndex
!= index
) {
383 const QString
sourceLabel( s_static
->completionSources
[ index
] );
384 if ( sections
.find( index
) == sections
.end() ) {
385 it
= items
.insert( it
, sourceLabel
);
386 ++it
; //skip new item
388 lastSourceIndex
= index
;
391 (*it
) = (*it
).prepend( s_completionItemIndentString
);
392 // remove preferred email sort <blank> added in addContact()
393 (*it
).replace( QLatin1String( " <" ), QLatin1String( " <" ) );
395 sections
[ index
].append( *it
);
397 if ( s_static
->completion
->order() == KCompletion::Sorted
) {
398 sortedItems
.append( *it
);
402 if ( s_static
->completion
->order() == KCompletion::Weighted
) {
405 QList
<SourceWithWeight
> sourcesAndWeights
;
406 for ( int i
= 0; i
< s_static
->completionSources
.size(); i
++ ) {
407 SourceWithWeight sww
;
408 sww
.sourceName
= s_static
->completionSources
[i
];
409 sww
.weight
= s_static
->completionSourceWeights
[sww
.sourceName
];
411 sourcesAndWeights
.append( sww
);
413 qSort( sourcesAndWeights
.begin(), sourcesAndWeights
.end() );
415 // Add the sections and their items to the final sortedItems result list
416 for ( int i
= 0; i
< sourcesAndWeights
.size(); i
++ ) {
417 const QStringList sectionItems
= sections
[sourcesAndWeights
[i
].index
];
418 if ( !sectionItems
.isEmpty() ) {
419 sortedItems
.append( sourcesAndWeights
[i
].sourceName
);
420 foreach ( const QString
&itemInSection
, sectionItems
) {
421 sortedItems
.append( itemInSection
);
432 void AddresseeLineEdit::Private::updateSearchString()
434 m_searchString
= q
->text();
437 bool inQuote
= false;
438 uint searchStringLength
= m_searchString
.length();
439 for ( uint i
= 0; i
< searchStringLength
; ++i
) {
440 if ( m_searchString
[ i
] == QLatin1Char( '"' ) ) {
444 if ( m_searchString
[ i
] == '\\' &&
445 ( i
+ 1 ) < searchStringLength
&& m_searchString
[ i
+ 1 ] == QLatin1Char( '"' ) ) {
453 if ( i
< searchStringLength
&&
454 ( m_searchString
[ i
] == ',' ||
455 ( m_useSemicolonAsSeparator
&& m_searchString
[ i
] == ';' ) ) ) {
461 ++n
; // Go past the ","
463 const int len
= m_searchString
.length();
465 // Increment past any whitespace...
466 while ( n
< len
&& m_searchString
[ n
].isSpace() ) {
470 m_previousAddresses
= m_searchString
.left( n
);
471 m_searchString
= m_searchString
.mid( n
).trimmed();
473 m_previousAddresses
.clear();
477 void AddresseeLineEdit::Private::akonadiPerformSearch()
479 kDebug() << "searching akonadi with:" << m_searchString
;
480 Akonadi::ContactSearchJob
*contactJob
= new Akonadi::ContactSearchJob();
481 Akonadi::ContactGroupSearchJob
*groupJob
= new Akonadi::ContactGroupSearchJob();
482 contactJob
->fetchScope().setAncestorRetrieval( Akonadi::ItemFetchScope::Parent
);
483 groupJob
->fetchScope().setAncestorRetrieval( Akonadi::ItemFetchScope::Parent
);
484 contactJob
->setQuery( Akonadi::ContactSearchJob::NameOrEmail
, m_searchString
,
485 Akonadi::ContactSearchJob::ContainsMatch
);
486 groupJob
->setQuery( Akonadi::ContactGroupSearchJob::Name
, m_searchString
,
487 Akonadi::ContactGroupSearchJob::StartsWithMatch
);
488 // FIXME: ContainsMatch is broken, even though it creates the correct SPARQL query, so use
489 // StartsWith for now
490 //Akonadi::ContactGroupSearchJob::ContainsMatch );
491 q
->connect( contactJob
, SIGNAL(result(KJob
*)),
492 q
, SLOT(slotAkonadiSearchResult(KJob
*)) );
493 q
->connect( groupJob
, SIGNAL(result(KJob
*)),
494 q
, SLOT(slotAkonadiSearchResult(KJob
*)) );
495 akonadiHandlePending();
498 void AddresseeLineEdit::Private::akonadiHandlePending()
500 kDebug() << "Pending items: " << s_static
->akonadiPendingItems
.size();
501 Akonadi::Item::List::iterator it
= s_static
->akonadiPendingItems
.begin();
502 while ( it
!= s_static
->akonadiPendingItems
.end() ) {
503 const Akonadi::Item item
= *it
;
505 const int sourceIndex
=
506 s_static
->akonadiCollectionToCompletionSourceMap
.value( item
.parentCollection().id(), -1 );
507 if ( sourceIndex
>= 0 ) {
508 kDebug() << "identified collection: " << s_static
->completionSources
[sourceIndex
];
509 q
->addItem( item
, 1, sourceIndex
);
511 // remove from the pending
512 it
= s_static
->akonadiPendingItems
.erase( it
);
519 void AddresseeLineEdit::Private::doCompletion( bool ctrlT
)
521 m_lastSearchMode
= ctrlT
;
523 const KGlobalSettings::Completion mode
= q
->completionMode();
525 if ( mode
== KGlobalSettings::CompletionNone
) {
529 s_static
->completion
->setOrder( completionOrder() );
531 // cursor at end of string - or Ctrl+T pressed for substring completion?
533 const QStringList completions
= adjustedCompletionItems( false );
535 if ( completions
.count() > 1 ) {
536 ; //m_previousAddresses = prevAddr;
537 } else if ( completions
.count() == 1 ) {
538 q
->setText( m_previousAddresses
+ completions
.first().trimmed() );
541 // Make sure the completion popup is closed if no matching items were found
542 setCompletedItems( completions
, true );
545 q
->setCompletionMode( mode
); //set back to previous mode
550 case KGlobalSettings::CompletionPopupAuto
:
552 if ( m_searchString
.isEmpty() ) {
555 //else: fall-through to the CompletionPopup case
558 case KGlobalSettings::CompletionPopup
:
560 const QStringList items
= adjustedCompletionItems( true );
561 setCompletedItems( items
, false );
565 case KGlobalSettings::CompletionShell
:
567 const QString match
= s_static
->completion
->makeCompletion( m_searchString
);
568 if ( !match
.isNull() && match
!= m_searchString
) {
569 q
->setText( m_previousAddresses
+ match
);
570 q
->setModified( true );
576 case KGlobalSettings::CompletionMan
: // Short-Auto in fact
577 case KGlobalSettings::CompletionAuto
:
579 //force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect
580 q
->setCompletionMode( q
->completionMode() );
582 if ( !m_searchString
.isEmpty() ) {
584 //if only our \" is left, remove it since user has not typed it either
585 if ( m_searchExtended
&& m_searchString
== QLatin1String( "\"" ) ) {
586 m_searchExtended
= false;
587 m_searchString
.clear();
588 q
->setText( m_previousAddresses
);
592 QString match
= s_static
->completion
->makeCompletion( m_searchString
);
594 if ( !match
.isEmpty() ) {
595 if ( match
!= m_searchString
) {
596 QString adds
= m_previousAddresses
+ match
;
597 q
->setCompletedText( adds
);
600 if ( !m_searchString
.startsWith( QLatin1Char( '\"' ) ) ) {
601 //try with quoted text, if user has not type one already
602 match
= s_static
->completion
->makeCompletion( QLatin1String( "\"" ) + m_searchString
);
603 if ( !match
.isEmpty() && match
!= m_searchString
) {
604 m_searchString
= QLatin1String( "\"" ) + m_searchString
;
605 m_searchExtended
= true;
606 q
->setText( m_previousAddresses
+ m_searchString
);
607 q
->setCompletedText( m_previousAddresses
+ match
);
609 } else if ( m_searchExtended
) {
610 //our added \" does not work anymore, remove it
611 m_searchString
= m_searchString
.mid( 1 );
612 m_searchExtended
= false;
613 q
->setText( m_previousAddresses
+ m_searchString
);
615 match
= s_static
->completion
->makeCompletion( m_searchString
);
616 if ( !match
.isEmpty() && match
!= m_searchString
) {
617 const QString adds
= m_previousAddresses
+ match
;
618 q
->setCompletedText( adds
);
626 case KGlobalSettings::CompletionNone
:
627 default: // fall through
632 void AddresseeLineEdit::Private::slotCompletion()
634 // Called by KLineEdit's keyPressEvent for CompletionModes
635 // Auto,Popup -> new text, update search string.
636 // not called for CompletionShell, this is been taken care of
637 // in AddresseeLineEdit::keyPressEvent
639 updateSearchString();
640 if ( q
->completionBox() ) {
641 q
->completionBox()->setCancelledText( m_searchString
);
644 akonadiPerformSearch();
645 doCompletion( false );
648 void AddresseeLineEdit::Private::slotPopupCompletion( const QString
&completion
)
650 q
->setText( m_previousAddresses
+ completion
.trimmed() );
652 updateSearchString();
653 q
->emitTextCompleted();
656 void AddresseeLineEdit::Private::slotReturnPressed( const QString
& )
658 if ( !q
->completionBox()->selectedItems().isEmpty() ) {
659 slotPopupCompletion( q
->completionBox()->selectedItems().first()->text() );
663 void AddresseeLineEdit::Private::slotStartLDAPLookup()
665 if ( Solid::Networking::status() == Solid::Networking::Unconnected
) {
669 const KGlobalSettings::Completion mode
= q
->completionMode();
671 if ( mode
== KGlobalSettings::CompletionNone
) {
675 if ( !s_static
->ldapSearch
->isAvailable() ) {
679 if ( s_static
->ldapLineEdit
!= q
) {
683 startLoadingLDAPEntries();
686 void AddresseeLineEdit::Private::slotLDAPSearchData( const KLDAP::LdapResult::List
&results
)
688 if ( results
.isEmpty() || s_static
->ldapLineEdit
!= q
) {
692 foreach ( const KLDAP::LdapResult
&result
, results
) {
693 KABC::Addressee contact
;
694 contact
.setNameFromString( result
.name
);
695 contact
.setEmails( result
.email
);
697 if ( !s_static
->ldapClientToCompletionSourceMap
.contains( result
.clientNumber
) ) {
698 updateLDAPWeights(); // we got results from a new source, so update the completion sources
701 q
->addContact( contact
, result
.completionWeight
,
702 s_static
->ldapClientToCompletionSourceMap
[ result
.clientNumber
] );
705 if ( ( q
->hasFocus() || q
->completionBox()->hasFocus() ) &&
706 q
->completionMode() != KGlobalSettings::CompletionNone
&&
707 q
->completionMode() != KGlobalSettings::CompletionShell
) {
708 q
->setText( m_previousAddresses
+ m_searchString
);
709 // only complete again if the user didn't change the selection while
710 // we were waiting; otherwise the completion box will be closed
711 const QListWidgetItem
*current
= q
->completionBox()->currentItem();
712 if ( !current
|| m_searchString
.trimmed() != current
->text().trimmed() ) {
713 doCompletion( m_lastSearchMode
);
718 void AddresseeLineEdit::Private::slotEditCompletionOrder()
720 init(); // for s_static->ldapSearch
722 CompletionOrderEditor
editor( s_static
->ldapSearch
, q
);
725 if ( m_useCompletion
) {
730 void AddresseeLineEdit::Private::slotUserCancelled( const QString
&cancelText
)
732 if ( s_static
->ldapSearch
&& s_static
->ldapLineEdit
== q
) {
736 q
->userCancelled( m_previousAddresses
+ cancelText
); // in KLineEdit
739 void AddresseeLineEdit::Private::slotAkonadiSearchResult( KJob
*job
)
741 const Akonadi::ContactSearchJob
*contactJob
=
742 qobject_cast
<Akonadi::ContactSearchJob
*>( job
);
743 const Akonadi::ContactGroupSearchJob
*groupJob
=
744 qobject_cast
<Akonadi::ContactGroupSearchJob
*>( job
);
747 kDebug() << "Found" << contactJob
->contacts().size() << "contacts";
748 } else if ( groupJob
) {
749 kDebug() << "Found" << groupJob
->contactGroups().size() << "groups";
752 /* We have to fetch the collections of the items, so that
753 the source name can be correctly labeled.*/
754 Akonadi::Item::List items
;
755 if ( contactJob
) items
+= contactJob
->items();
756 if ( groupJob
) items
+= groupJob
->items();
757 foreach ( const Akonadi::Item
&item
, items
) {
759 // check the local cache of collections
760 const int sourceIndex
=
761 s_static
->akonadiCollectionToCompletionSourceMap
.value( item
.parentCollection().id(), -1 );
762 if ( sourceIndex
== -1 ) {
763 kDebug() << "Fetching New collection: " << item
.parentCollection().id();
764 // the collection isn't there, start the fetch job.
765 Akonadi::CollectionFetchJob
*collectionJob
=
766 new Akonadi::CollectionFetchJob( item
.parentCollection(),
767 Akonadi::CollectionFetchJob::Base
, q
);
768 connect( collectionJob
, SIGNAL(collectionsReceived(Akonadi::Collection::List
)),
769 q
, SLOT(slotAkonadiCollectionsReceived(Akonadi::Collection::List
)) );
770 /* we don't want to start multiple fetch jobs for the same collection,
771 so insert the collection with an index value of -2 */
772 s_static
->akonadiCollectionToCompletionSourceMap
.insert( item
.parentCollection().id(), -2 );
773 s_static
->akonadiPendingItems
.append( item
);
774 } else if ( sourceIndex
== -2 ) {
775 /* fetch job already started, don't need to start another one,
776 so just append the item as pending */
777 s_static
->akonadiPendingItems
.append( item
);
779 q
->addItem( item
, 1, sourceIndex
);
783 if ( ( contactJob
&& contactJob
->contacts().size() > 0 ) ||
784 ( groupJob
&& groupJob
->contactGroups().size() > 0 ) ) {
785 const QListWidgetItem
*current
= q
->completionBox()->currentItem();
786 if ( !current
|| m_searchString
.trimmed() != current
->text().trimmed() ) {
787 doCompletion( m_lastSearchMode
);
792 void AddresseeLineEdit::Private::slotAkonadiCollectionsReceived(
793 const Akonadi::Collection::List
&collections
)
795 foreach ( const Akonadi::Collection
&collection
, collections
) {
796 if ( collection
.isValid() ) {
797 const Akonadi::EntityDisplayAttribute
*attribute
=
798 collection
.attribute
<Akonadi::EntityDisplayAttribute
>();
800 QString sourceString
= collection
.name();
801 if ( attribute
&& !attribute
->displayName().isEmpty() )
802 sourceString
= attribute
->displayName();
804 const int index
= q
->addCompletionSource( sourceString
, 1 );
805 kDebug() << "\treceived: " << sourceString
<< "index: " << index
;
806 s_static
->akonadiCollectionToCompletionSourceMap
.insert( collection
.id(), index
);
810 // now that we have added the new collections, recheck our list of pending contacts
811 akonadiHandlePending();
813 const QListWidgetItem
*current
= q
->completionBox()->currentItem();
814 if ( !current
|| m_searchString
.trimmed() != current
->text().trimmed() ) {
815 doCompletion( m_lastSearchMode
);
819 // not cached, to make sure we get an up-to-date value when it changes
820 KCompletion::CompOrder
AddresseeLineEdit::Private::completionOrder()
822 KConfig
_config( QLatin1String( "kpimcompletionorder" ) );
823 const KConfigGroup
config( &_config
, QLatin1String( "General" ) );
824 const QString order
=
825 config
.readEntry( QLatin1String( "CompletionOrder" ), QString::fromLatin1( "Weighted" ) );
827 if ( order
== QLatin1String( "Weighted" ) ) {
828 return KCompletion::Weighted
;
830 return KCompletion::Sorted
;
834 AddresseeLineEdit::AddresseeLineEdit( QWidget
*parent
, bool enableCompletion
)
835 : KLineEdit( parent
), d( new Private( this, enableCompletion
) )
837 setObjectName( newLineEditObjectName() );
838 setClickMessage( QString() );
843 AddresseeLineEdit::~AddresseeLineEdit()
845 if ( s_static
->ldapSearch
&& s_static
->ldapLineEdit
== this ) {
852 void AddresseeLineEdit::setFont( const QFont
&font
)
854 KLineEdit::setFont( font
);
856 if ( d
->m_useCompletion
) {
857 completionBox()->setFont( font
);
861 void AddresseeLineEdit::allowSemicolonAsSeparator( bool useSemicolonAsSeparator
)
863 d
->m_useSemicolonAsSeparator
= useSemicolonAsSeparator
;
866 void AddresseeLineEdit::keyPressEvent( QKeyEvent
*event
)
870 const int key
= event
->key() | event
->modifiers();
872 if ( KStandardShortcut::shortcut( KStandardShortcut::SubstringCompletion
).contains( key
) ) {
873 //TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch
874 d
->updateSearchString();
875 d
->akonadiPerformSearch();
876 d
->doCompletion( true );
878 } else if ( KStandardShortcut::shortcut( KStandardShortcut::TextCompletion
).contains( key
) ) {
879 const int len
= text().length();
881 if ( len
== cursorPosition() ) { // at End?
882 d
->updateSearchString();
883 d
->akonadiPerformSearch();
884 d
->doCompletion( true );
889 const QString oldContent
= text();
891 KLineEdit::keyPressEvent( event
);
894 // if the text didn't change (eg. because a cursor navigation key was pressed)
895 // we don't need to trigger a new search
896 if ( oldContent
== text() ) {
900 if ( event
->isAccepted() ) {
901 d
->updateSearchString();
903 QString
searchString( d
->m_searchString
);
904 //LDAP does not know about our string manipulation, remove it
905 if ( d
->m_searchExtended
) {
906 searchString
= d
->m_searchString
.mid( 1 );
909 if ( d
->m_useCompletion
&& s_static
->ldapTimer
) {
910 if ( s_static
->ldapText
!= searchString
|| s_static
->ldapLineEdit
!= this ) {
914 s_static
->ldapText
= searchString
;
915 s_static
->ldapLineEdit
= this;
916 s_static
->ldapTimer
->setSingleShot( true );
917 s_static
->ldapTimer
->start( 500 );
922 void AddresseeLineEdit::insert( const QString
&t
)
924 if ( !d
->m_smartPaste
) {
925 KLineEdit::insert( t
);
929 QString newText
= t
.trimmed();
930 if ( newText
.isEmpty() ) {
934 // remove newlines in the to-be-pasted string
935 QStringList lines
= newText
.split( QRegExp( QLatin1String( "\r?\n" ) ), QString::SkipEmptyParts
);
936 for ( QStringList::iterator it
= lines
.begin(); it
!= lines
.end(); ++it
) {
937 // remove trailing commas and whitespace
938 (*it
).remove( QRegExp( QLatin1String( ",?\\s*$" ) ) );
940 newText
= lines
.join( QLatin1String( ", " ) );
942 if ( newText
.startsWith( QLatin1String( "mailto:" ) ) ) {
943 const KUrl
url( newText
);
944 newText
= url
.path();
945 } else if ( newText
.indexOf( QLatin1String( " at " ) ) != -1 ) {
947 newText
.replace( QLatin1String( " at " ), QLatin1String( "@" ) );
948 newText
.replace( QLatin1String( " dot " ), QLatin1String( "." ) );
949 } else if ( newText
.indexOf( QLatin1String( "(at)" ) ) != -1 ) {
950 newText
.replace( QRegExp( QLatin1String( "\\s*\\(at\\)\\s*" ) ), QLatin1String( "@" ) );
953 QString contents
= text();
955 int pos
= cursorPosition();
957 if ( hasSelectedText() ) {
958 // Cut away the selection.
959 start_sel
= selectionStart();
961 contents
= contents
.left( start_sel
) + contents
.mid( start_sel
+ selectedText().length() );
964 int eot
= contents
.length();
965 while ( ( eot
> 0 ) && contents
[ eot
- 1 ].isSpace() ) {
970 } else if ( pos
>= eot
) {
971 if ( contents
[ eot
- 1 ] == QLatin1Char( ',' ) ) {
974 contents
.truncate( eot
);
975 contents
+= QLatin1String( ", " );
979 contents
= contents
.left( pos
) + newText
+ contents
.mid( pos
);
982 setCursorPosition( pos
+ newText
.length() );
985 void AddresseeLineEdit::setText( const QString
& text
)
987 KLineEdit::setText( text
.trimmed() );
990 void AddresseeLineEdit::paste()
992 if ( d
->m_useCompletion
) {
993 d
->m_smartPaste
= true;
996 #ifndef QT_NO_CLIPBOARD
999 d
->m_smartPaste
= false;
1002 void AddresseeLineEdit::mouseReleaseEvent( QMouseEvent
*event
)
1004 // reimplemented from QLineEdit::mouseReleaseEvent()
1005 #ifndef QT_NO_CLIPBOARD
1006 if ( d
->m_useCompletion
&&
1007 QApplication::clipboard()->supportsSelection() &&
1009 event
->button() == Qt::MidButton
) {
1010 d
->m_smartPaste
= true;
1014 KLineEdit::mouseReleaseEvent( event
);
1015 d
->m_smartPaste
= false;
1018 #ifndef QT_NO_DRAGANDDROP
1019 void AddresseeLineEdit::dropEvent( QDropEvent
*event
)
1021 if ( !isReadOnly() ) {
1022 const KUrl::List uriList
= KUrl::List::fromMimeData( event
->mimeData() );
1023 if ( !uriList
.isEmpty() ) {
1024 QString contents
= text();
1025 // remove trailing white space and comma
1026 int eot
= contents
.length();
1027 while ( ( eot
> 0 ) && contents
[ eot
- 1 ].isSpace() ) {
1032 } else if ( contents
[ eot
- 1 ] == ',' ) {
1034 contents
.truncate( eot
);
1036 bool mailtoURL
= false;
1037 // append the mailto URLs
1038 foreach ( const KUrl
&url
, uriList
) {
1039 if ( url
.protocol() == QLatin1String( "mailto" ) ) {
1042 address
= KUrl::fromPercentEncoding( url
.path().toLatin1() );
1043 address
= KMime::decodeRFC2047String( address
.toAscii() );
1044 if ( !contents
.isEmpty() ) {
1045 contents
.append( QLatin1String( ", " ) );
1047 contents
.append( address
);
1051 setText( contents
);
1052 setModified( true );
1056 // Let's see if this drop contains a comma separated list of emails
1057 QString dropData
= QString::fromUtf8( event
->encodedData( "text/plain" ) );
1058 QStringList addrs
= KPIMUtils::splitAddressList( dropData
);
1059 if ( addrs
.count() > 0 ) {
1060 setText( KPIMUtils::normalizeAddressesAndDecodeIdn( dropData
) );
1061 setModified( true );
1067 if ( d
->m_useCompletion
) {
1068 d
->m_smartPaste
= true;
1071 QLineEdit::dropEvent( event
);
1072 d
->m_smartPaste
= false;
1074 #endif // QT_NO_DRAGANDDROP
1076 void AddresseeLineEdit::cursorAtEnd()
1078 setCursorPosition( text().length() );
1081 void AddresseeLineEdit::enableCompletion( bool enable
)
1083 d
->m_useCompletion
= enable
;
1086 void AddresseeLineEdit::addItem( const Akonadi::Item
&item
, int weight
, int source
)
1088 if ( item
.hasPayload
<KABC::Addressee
>() ) {
1089 addContact( item
.payload
<KABC::Addressee
>(), weight
, source
);
1090 } else if ( item
.hasPayload
<KABC::ContactGroup
>() ) {
1091 addContactGroup( item
.payload
<KABC::ContactGroup
>(), weight
, source
);
1095 void AddresseeLineEdit::addContactGroup( const KABC::ContactGroup
&group
, int weight
, int source
)
1097 d
->addCompletionItem( group
.name(), weight
, source
);
1098 QStringList keyWords
;
1099 keyWords
.append( group
.name() );
1100 d
->addCompletionItem( group
.name(), weight
, source
, &keyWords
);
1103 void AddresseeLineEdit::addContact( const KABC::Addressee
&addr
, int weight
, int source
)
1105 const QStringList emails
= addr
.emails();
1106 QStringList::ConstIterator it
;
1107 const int prefEmailWeight
= 1; //increment weight by prefEmailWeight
1108 int isPrefEmail
= prefEmailWeight
; //first in list is preferredEmail
1109 for ( it
= emails
.begin(); it
!= emails
.end(); ++it
) {
1110 //TODO: highlight preferredEmail
1111 const QString
email( (*it
) );
1112 const QString givenName
= addr
.givenName();
1113 const QString familyName
= addr
.familyName();
1114 const QString nickName
= addr
.nickName();
1115 const QString domain
= email
.mid( email
.indexOf( '@' ) + 1 );
1116 QString fullEmail
= addr
.fullEmail( email
);
1117 //TODO: let user decide what fields to use in lookup, e.g. company, city, ...
1118 //for CompletionAuto
1119 if ( givenName
.isEmpty() && familyName
.isEmpty() ) {
1120 d
->addCompletionItem( fullEmail
, weight
+ isPrefEmail
, source
); // use whatever is there
1122 const QString byFirstName
= QLatin1Char( '"' ) + givenName
+
1123 QLatin1Char( ' ' ) + familyName
+
1124 QLatin1String( "\" <" ) + email
+ QLatin1Char( '>' );
1125 const QString byLastName
= QLatin1Char( '"' ) + familyName
+
1126 QLatin1String( ", " ) + givenName
+
1127 QLatin1String( "\" <" ) + email
+ QLatin1Char( '>' );
1128 d
->addCompletionItem( byFirstName
, weight
+ isPrefEmail
, source
);
1129 d
->addCompletionItem( byLastName
, weight
+ isPrefEmail
, source
);
1132 d
->addCompletionItem( email
, weight
+ isPrefEmail
, source
);
1134 if ( !nickName
.isEmpty() ) {
1135 const QString byNick
= QLatin1Char( '"' ) + nickName
+
1136 QLatin1String( "\" <" ) + email
+ QLatin1Char( '>' );
1137 d
->addCompletionItem( byNick
, weight
+ isPrefEmail
, source
);
1140 if ( !domain
.isEmpty() ) {
1141 const QString byDomain
= QLatin1Char( '"' ) + domain
+ QLatin1Char( ' ' ) +
1142 familyName
+ QLatin1Char( ' ' ) + givenName
+
1143 QLatin1String( "\" <" ) + email
+ QLatin1Char( '>' );
1144 d
->addCompletionItem( byDomain
, weight
+ isPrefEmail
, source
);
1147 //for CompletionShell, CompletionPopup
1148 QStringList keyWords
;
1149 const QString realName
= addr
.realName();
1151 if ( !givenName
.isEmpty() && !familyName
.isEmpty() ) {
1152 keyWords
.append( givenName
+ QLatin1Char( ' ' ) + familyName
);
1153 keyWords
.append( familyName
+ QLatin1Char( ' ' ) + givenName
);
1154 keyWords
.append( familyName
+ QLatin1String( ", " ) + givenName
);
1155 } else if ( !givenName
.isEmpty() ) {
1156 keyWords
.append( givenName
);
1157 } else if ( !familyName
.isEmpty() ) {
1158 keyWords
.append( familyName
);
1161 if ( !nickName
.isEmpty() ) {
1162 keyWords
.append( nickName
);
1165 if ( !realName
.isEmpty() ) {
1166 keyWords
.append( realName
);
1169 if ( !domain
.isEmpty() ) {
1170 keyWords
.append( domain
);
1173 keyWords
.append( email
);
1175 /* KMailCompletion does not have knowledge about identities, it stores emails and
1176 * keywords for each email. KMailCompletion::allMatches does a lookup on the
1177 * keywords and returns an ordered list of emails. In order to get the preferred
1178 * email before others for each identity we use this little trick.
1179 * We remove the <blank> in adjustedCompletionItems.
1181 if ( isPrefEmail
== prefEmailWeight
) {
1182 fullEmail
.replace( QLatin1String( " <" ), QLatin1String( " <" ) );
1185 d
->addCompletionItem( fullEmail
, weight
+ isPrefEmail
, source
, &keyWords
);
1190 #ifndef QT_NO_CONTEXTMENU
1191 void AddresseeLineEdit::contextMenuEvent( QContextMenuEvent
*event
)
1193 QMenu
*menu
= createStandardContextMenu();
1194 if ( menu
) { // can be 0 on platforms with only a touch interface
1195 menu
->exec( event
->globalPos() );
1200 QMenu
*AddresseeLineEdit::createStandardContextMenu()
1202 // disable modes not supported by KMailCompletion
1203 setCompletionModeDisabled( KGlobalSettings::CompletionMan
);
1204 setCompletionModeDisabled( KGlobalSettings::CompletionPopupAuto
);
1206 QMenu
*menu
= KLineEdit::createStandardContextMenu();
1211 if ( d
->m_useCompletion
) {
1212 menu
->addAction( i18n( "Configure Completion Order..." ),
1213 this, SLOT(slotEditCompletionOrder()) );
1219 int KPIM::AddresseeLineEdit::addCompletionSource( const QString
&source
, int weight
)
1221 QMap
<QString
,int>::iterator it
= s_static
->completionSourceWeights
.find( source
);
1222 if ( it
== s_static
->completionSourceWeights
.end() ) {
1223 s_static
->completionSourceWeights
.insert( source
, weight
);
1225 s_static
->completionSourceWeights
[source
] = weight
;
1228 const int sourceIndex
= s_static
->completionSources
.indexOf( source
);
1229 if ( sourceIndex
== -1 ) {
1230 s_static
->completionSources
.append( source
);
1231 return s_static
->completionSources
.size() - 1;
1237 bool KPIM::AddresseeLineEdit::eventFilter( QObject
*object
, QEvent
*event
)
1239 if ( d
->m_completionInitialized
&&
1240 ( object
== completionBox() ||
1241 completionBox()->findChild
<QWidget
*>( object
->objectName() ) == object
) ) {
1242 if ( event
->type() == QEvent::MouseButtonPress
||
1243 event
->type() == QEvent::MouseMove
||
1244 event
->type() == QEvent::MouseButtonRelease
||
1245 event
->type() == QEvent::MouseButtonDblClick
) {
1247 const QMouseEvent
* mouseEvent
= static_cast<QMouseEvent
*>( event
);
1248 // find list box item at the event position
1249 QListWidgetItem
*item
= completionBox()->itemAt( mouseEvent
->pos() );
1251 // In the case of a mouse move outside of the box we don't want
1252 // the parent to fuzzy select a header by mistake.
1253 bool eat
= event
->type() == QEvent::MouseMove
;
1256 // avoid selection of headers on button press, or move or release while
1257 // a button is pressed
1258 const Qt::MouseButtons buttons
= mouseEvent
->buttons();
1259 if ( event
->type() == QEvent::MouseButtonPress
||
1260 event
->type() == QEvent::MouseButtonDblClick
||
1261 buttons
& Qt::LeftButton
|| buttons
& Qt::MidButton
||
1262 buttons
& Qt::RightButton
) {
1263 if ( itemIsHeader( item
) ) {
1264 return true; // eat the event, we don't want anything to happen
1266 // if we are not on one of the group heading, make sure the item
1267 // below or above is selected, not the heading, inadvertedly, due
1268 // to fuzzy auto-selection from QListBox
1269 completionBox()->setCurrentItem( item
);
1270 item
->setSelected( true );
1271 if ( event
->type() == QEvent::MouseMove
) {
1272 return true; // avoid fuzzy selection behavior
1279 if ( ( object
== this ) &&
1280 ( event
->type() == QEvent::ShortcutOverride
) ) {
1281 QKeyEvent
*keyEvent
= static_cast<QKeyEvent
*>( event
);
1282 if ( keyEvent
->key() == Qt::Key_Up
|| keyEvent
->key() == Qt::Key_Down
||
1283 keyEvent
->key() == Qt::Key_Tab
) {
1289 if ( ( object
== this ) &&
1290 ( event
->type() == QEvent::KeyPress
|| event
->type() == QEvent::KeyRelease
) &&
1291 completionBox()->isVisible() ) {
1292 const QKeyEvent
*keyEvent
= static_cast<QKeyEvent
*>( event
);
1293 int currentIndex
= completionBox()->currentRow();
1294 if ( currentIndex
< 0 ) {
1297 if ( keyEvent
->key() == Qt::Key_Up
) {
1298 //kDebug() <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex;
1299 // figure out if the item we would be moving to is one we want
1300 // to ignore. If so, go one further
1301 const QListWidgetItem
*itemAbove
= completionBox()->item( currentIndex
);
1302 if ( itemAbove
&& itemIsHeader( itemAbove
) ) {
1303 // there is a header above is, check if there is even further up
1304 // and if so go one up, so it'll be selected
1305 if ( currentIndex
> 0 && completionBox()->item( currentIndex
- 1 ) ) {
1306 //kDebug() <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1;
1307 completionBox()->setCurrentRow( currentIndex
- 1 );
1308 completionBox()->item( currentIndex
- 1 )->setSelected( true );
1309 } else if ( currentIndex
== 0 ) {
1310 // nothing to skip to, let's stay where we are, but make sure the
1311 // first header becomes visible, if we are the first real entry
1312 completionBox()->scrollToItem( completionBox()->item( 0 ) );
1313 QListWidgetItem
*item
= completionBox()->item( currentIndex
);
1315 if ( itemIsHeader( item
) ) {
1317 item
= completionBox()->item( currentIndex
);
1319 completionBox()->setCurrentItem( item
);
1320 item
->setSelected( true );
1326 } else if ( keyEvent
->key() == Qt::Key_Down
) {
1327 // same strategy for downwards
1328 //kDebug() <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex;
1329 const QListWidgetItem
*itemBelow
= completionBox()->item( currentIndex
);
1330 if ( itemBelow
&& itemIsHeader( itemBelow
) ) {
1331 if ( completionBox()->item( currentIndex
+ 1 ) ) {
1332 //kDebug() <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1;
1333 completionBox()->setCurrentRow( currentIndex
+ 1 );
1334 completionBox()->item( currentIndex
+ 1 )->setSelected( true );
1336 // nothing to skip to, let's stay where we are
1337 QListWidgetItem
*item
= completionBox()->item( currentIndex
);
1339 completionBox()->setCurrentItem( item
);
1340 item
->setSelected( true );
1346 // special case of the initial selection, which is unfortunately a header.
1347 // Setting it to selected tricks KCompletionBox into not treating is special
1348 // and selecting making it current, instead of the one below.
1349 QListWidgetItem
*item
= completionBox()->item( currentIndex
);
1350 if ( item
&& itemIsHeader( item
) ) {
1351 completionBox()->setCurrentItem( item
);
1352 item
->setSelected( true );
1354 } else if ( event
->type() == QEvent::KeyRelease
&&
1355 ( keyEvent
->key() == Qt::Key_Tab
|| keyEvent
->key() == Qt::Key_Backtab
) ) {
1356 /// first, find the header of the current section
1357 QListWidgetItem
*myHeader
= 0;
1358 int myHeaderIndex
= -1;
1359 const int iterationStep
= keyEvent
->key() == Qt::Key_Tab
? 1 : -1;
1360 int index
= qMin( qMax( currentIndex
- iterationStep
, 0 ), completionBox()->count() - 1 );
1361 while ( index
>= 0 ) {
1362 if ( itemIsHeader( completionBox()->item( index
) ) ) {
1363 myHeader
= completionBox()->item( index
);
1364 myHeaderIndex
= index
;
1370 Q_ASSERT( myHeader
); // we should always be able to find a header
1372 // find the next header (searching backwards, for Qt::Key_Backtab)
1373 QListWidgetItem
*nextHeader
= 0;
1375 // when iterating forward, start at the currentindex, when backwards,
1376 // one up from our header, or at the end
1378 if ( keyEvent
->key() == Qt::Key_Tab
) {
1381 index
= myHeaderIndex
;
1383 j
= completionBox()->count() - 1;
1385 j
= ( index
- 1 ) % completionBox()->count();
1388 while ( ( nextHeader
= completionBox()->item( j
) ) && nextHeader
!= myHeader
) {
1389 if ( itemIsHeader( nextHeader
) ) {
1392 j
= ( j
+ iterationStep
) % completionBox()->count();
1395 if ( nextHeader
&& nextHeader
!= myHeader
) {
1396 QListWidgetItem
*item
= completionBox()->item( j
+ 1 );
1397 if ( item
&& !itemIsHeader( item
) ) {
1398 completionBox()->setCurrentItem( item
);
1399 item
->setSelected( true );
1407 return KLineEdit::eventFilter( object
, event
);
1410 void AddresseeLineEdit::emitTextCompleted()
1412 emit
textCompleted();
1416 #include "addresseelineedit.moc"