• Skip to content
  • Skip to link menu
KDE API Reference
  • KDE API Reference
  • kdepim API Reference
  • KDE Home
  • Contact Us
 

libkdepim

  • sources
  • kde-4.12
  • kdepim
  • libkdepim
  • addressline
addresseelineedit.cpp
Go to the documentation of this file.
1 /*
2  This file is part of libkdepim.
3 
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>
10 
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.
15 
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.
20 
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.
25 */
26 
27 #include "addresseelineedit.h"
28 #include "ldap/ldapclientsearch.h"
29 #ifndef Q_OS_WINCE
30 #include "completionordereditor.h"
31 #endif
32 
33 #include "kmailcompletion.h"
34 
35 #include <Akonadi/Contact/ContactSearchJob>
36 #include <Akonadi/Contact/ContactGroupSearchJob>
37 #include <Akonadi/CollectionFetchJob>
38 #include <Akonadi/EntityDisplayAttribute>
39 #include <Akonadi/ItemFetchScope>
40 #include <Akonadi/RecursiveItemFetchJob>
41 #include <Akonadi/Session>
42 #include <Akonadi/Job>
43 
44 #include <Nepomuk2/Query/Query>
45 #include <Nepomuk2/Query/QueryServiceClient>
46 #include <Nepomuk2/Query/Result>
47 #include <Nepomuk2/Resource>
48 #include <Nepomuk2/Vocabulary/NCO>
49 #include <Nepomuk2/ResourceManager>
50 
51 #include <KPIMUtils/Email>
52 
53 #include <KLDAP/LdapServer>
54 
55 #include <KMime/Util>
56 
57 #include <KConfigGroup>
58 #include <KCompletionBox>
59 #include <KDebug>
60 #include <KLocale>
61 #include <KStandardDirs>
62 #include <KStandardShortcut>
63 #include <KUrl>
64 
65 #include <solid/networking.h>
66 
67 #include <QApplication>
68 #include <QCursor>
69 #include <QObject>
70 #include <QRegExp>
71 #include <QEvent>
72 #include <QClipboard>
73 #include <QKeyEvent>
74 #include <QDropEvent>
75 #include <QMouseEvent>
76 #include <QMenu>
77 #include <QTimer>
78 #include <QtDBus/QDBusConnection>
79 
80 using namespace KPIM;
81 
82 namespace KPIM {
83  typedef QMap< QString, QPair<int,int> > CompletionItemsMap;
84 }
85 
86 class AddresseeLineEditStatic
87 {
88  public:
89 
90  AddresseeLineEditStatic()
91  : completion( new KMailCompletion ),
92  ldapTimer( 0 ),
93  ldapSearch( 0 ),
94  ldapLineEdit( 0 ),
95  nepomukSearchClient( 0 ),
96  akonadiSession( new Akonadi::Session("contactsCompletionSession") ),
97  nepomukCompletionSource( 0 ),
98  contactsListed( false )
99  {
100  KConfig config( QLatin1String( "kpimcompletionorder" ) );
101  const KConfigGroup group( &config, QLatin1String( "General" ) );
102  useNepomukCompletion = group.readEntry( "UseNepomuk", true );
103  }
104 
105  ~AddresseeLineEditStatic()
106  {
107  delete completion;
108  delete ldapTimer;
109  delete ldapSearch;
110  delete nepomukSearchClient;
111  }
112 
113  void slotEditCompletionOrder()
114  {
115  CompletionOrderEditor editor( ldapSearch, 0 );
116  if( editor.exec() ) {
117  updateLDAPWeights();
118  }
119  }
120 
121  void updateLDAPWeights()
122  {
123  /* Add completion sources for all ldap server, 0 to n. Added first so
124  * that they map to the LdapClient::clientNumber() */
125  ldapSearch->updateCompletionWeights();
126  int clientIndex = 0;
127  foreach ( const KLDAP::LdapClient *client, ldapSearch->clients() ) {
128  const int sourceIndex =
129  addCompletionSource( i18n( "LDAP server: %1" ,client->server().host()),
130  client->completionWeight() );
131 
132  ldapClientToCompletionSourceMap.insert( clientIndex, sourceIndex );
133 
134  clientIndex++;
135  }
136  }
137 
138  int addCompletionSource( const QString &source, int weight )
139  {
140  QMap<QString,int>::iterator it = completionSourceWeights.find( source );
141  if ( it == completionSourceWeights.end() ) {
142  completionSourceWeights.insert( source, weight );
143  } else {
144  completionSourceWeights[source] = weight;
145  }
146 
147  const int sourceIndex = completionSources.indexOf( source );
148  if ( sourceIndex == -1 ) {
149  completionSources.append( source );
150  return completionSources.size() - 1;
151  } else {
152  return sourceIndex;
153  }
154  }
155 
156  void removeCompletionSource( const QString &source )
157  {
158  QMap<QString,int>::iterator it = completionSourceWeights.find( source );
159  if ( it != completionSourceWeights.end() ) {
160  completionSourceWeights.remove(source);
161  completion->clear();
162  }
163  }
164 
165  KMailCompletion *completion;
166  KPIM::CompletionItemsMap completionItemMap;
167  QStringList completionSources;
168  QTimer *ldapTimer;
169  KLDAP::LdapClientSearch *ldapSearch;
170  QString ldapText;
171  AddresseeLineEdit *ldapLineEdit;
172  // The weights associated with the completion sources in s_static->completionSources.
173  // Both are maintained by addCompletionSource(), don't attempt to modifiy those yourself.
174  QMap<QString, int> completionSourceWeights;
175  // maps LDAP client indices to completion source indices
176  // the assumption that they are always the first n indices in s_static->completion
177  // does not hold when clients are added later on
178  QMap<int, int> ldapClientToCompletionSourceMap;
179  // holds the cached mapping from akonadi collection id to the completion source index
180  QMap<Akonadi::Collection::Id, int> akonadiCollectionToCompletionSourceMap;
181  // a list of akonadi items (contacts) that have not had their collection fetched yet
182  Akonadi::Item::List akonadiPendingItems;
183  Nepomuk2::Query::QueryServiceClient* nepomukSearchClient;
184  Akonadi::Session *akonadiSession;
185  QVector<QWeakPointer<Akonadi::Job> > akonadiJobsInFlight;
186  bool useNepomukCompletion;
187  int nepomukCompletionSource;
188  bool contactsListed;
189 };
190 
191 K_GLOBAL_STATIC( AddresseeLineEditStatic, s_static )
192 
193 // needs to be unique, but the actual name doesn't matter much
194 static QString newLineEditObjectName()
195 {
196  static int s_count = 0;
197  QString name( QLatin1String("KPIM::AddresseeLineEdit") );
198  if ( s_count++ ) {
199  name += QLatin1Char('-');
200  name += QString::number( s_count );
201  }
202  return name;
203 }
204 
205 static const QString s_completionItemIndentString = QLatin1String(" ");
206 
207 static bool itemIsHeader( const QListWidgetItem *item )
208 {
209  return item && !item->text().startsWith( s_completionItemIndentString );
210 }
211 
212 class SourceWithWeight
213 {
214  public:
215  int weight; // the weight of the source
216  QString sourceName; // the name of the source, e.g. "LDAP Server"
217  int index; // index into s_static->completionSources
218 
219  bool operator< ( const SourceWithWeight &other ) const
220  {
221  if ( weight > other.weight ) {
222  return true;
223  }
224 
225  if ( weight < other.weight ) {
226  return false;
227  }
228 
229  return sourceName < other.sourceName;
230  }
231 };
232 
233 class AddresseeLineEdit::Private
234 {
235  public:
236  Private( AddresseeLineEdit *qq, bool enableCompletion )
237  : q( qq ),
238  m_useCompletion( enableCompletion ),
239  m_completionInitialized( false ),
240  m_smartPaste( false ),
241  m_addressBookConnected( false ),
242  m_lastSearchMode( false ),
243  m_searchExtended( false ),
244  m_useSemicolonAsSeparator( false )
245  {
246  m_delayedQueryTimer.setSingleShot(true);
247  connect( &m_delayedQueryTimer, SIGNAL(timeout()), q, SLOT(slotTriggerDelayedQueries()) );
248  }
249 
250  void init();
251  void startLoadingLDAPEntries();
252  void stopLDAPLookup();
253  void setCompletedItems( const QStringList &items, bool autoSuggest );
254  void addCompletionItem( const QString &string, int weight, int source,
255  const QStringList *keyWords = 0 );
256  const QStringList adjustedCompletionItems( bool fullSearch );
257  void updateSearchString();
258  void startSearches();
259  void akonadiPerformSearch();
260  void akonadiListAllContacts();
261  void akonadiHandlePending();
262  void akonadiHandleItems( const Akonadi::Item::List &items );
263  void doCompletion( bool ctrlT );
264 
265  void slotCompletion();
266  void slotPopupCompletion( const QString & );
267  void slotReturnPressed( const QString & );
268  void slotStartLDAPLookup();
269  void slotLDAPSearchData( const KLDAP::LdapResult::List & );
270  void slotEditCompletionOrder();
271  void slotUserCancelled( const QString & );
272  void slotAkonadiSearchResult( KJob * );
273  void slotAkonadiSearchDbResult( KJob* );
274  void slotAkonadiCollectionsReceived( const Akonadi::Collection::List & );
275  void startNepomukSearch();
276  void stopNepomukSearch();
277  void slotNepomukHits( const QList<Nepomuk2::Query::Result>& result );
278  void slotNepomukSearchFinished();
279  void slotTriggerDelayedQueries();
280  static KCompletion::CompOrder completionOrder();
281 
282  AddresseeLineEdit *q;
283  QString m_previousAddresses;
284  QString m_searchString;
285  bool m_useCompletion;
286  bool m_completionInitialized;
287  bool m_smartPaste;
288  bool m_addressBookConnected;
289  bool m_lastSearchMode;
290  bool m_searchExtended; //has \" been added?
291  bool m_useSemicolonAsSeparator;
292  QTimer m_delayedQueryTimer;
293 };
294 
295 void AddresseeLineEdit::Private::init()
296 {
297  if ( !s_static.exists() ) {
298  s_static->completion->setOrder( completionOrder() );
299  s_static->completion->setIgnoreCase( true );
300  }
301 
302  if ( m_useCompletion ) {
303  if ( !s_static->ldapTimer ) {
304  s_static->ldapTimer = new QTimer;
305  s_static->ldapSearch = new KLDAP::LdapClientSearch;
306 
307  }
308 
309  if ( !s_static->nepomukSearchClient ) {
310  s_static->nepomukSearchClient = new Nepomuk2::Query::QueryServiceClient;
311  s_static->nepomukCompletionSource = q->addCompletionSource( i18nc( "@title:group", "Contacts found in your data"), -1 );
312  }
313 
314  s_static->updateLDAPWeights();
315 
316  if ( !m_completionInitialized ) {
317  q->setCompletionObject( s_static->completion, false );
318  q->connect( q, SIGNAL(completion(QString)),
319  q, SLOT(slotCompletion()) );
320  q->connect( q, SIGNAL(returnPressed(QString)),
321  q, SLOT(slotReturnPressed(QString)) );
322 
323  KCompletionBox *box = q->completionBox();
324  q->connect( box, SIGNAL(activated(QString)),
325  q, SLOT(slotPopupCompletion(QString)) );
326  q->connect( box, SIGNAL(userCancelled(QString)),
327  q, SLOT(slotUserCancelled(QString)) );
328 
329  q->connect( s_static->ldapTimer, SIGNAL(timeout()), SLOT(slotStartLDAPLookup()) );
330  q->connect( s_static->ldapSearch, SIGNAL(searchData(KLDAP::LdapResult::List)),
331  SLOT(slotLDAPSearchData(KLDAP::LdapResult::List)) );
332 
333  q->connect( s_static->nepomukSearchClient, SIGNAL(newEntries(QList<Nepomuk2::Query::Result>)),
334  q, SLOT(slotNepomukHits(QList<Nepomuk2::Query::Result>)) );
335  q->connect( s_static->nepomukSearchClient, SIGNAL(finishedListing()), q, SLOT(slotNepomukSearchFinished()));
336  m_completionInitialized = true;
337  }
338  }
339 }
340 
341 void AddresseeLineEdit::Private::startLoadingLDAPEntries()
342 {
343  QString text( s_static->ldapText );
344 
345  // TODO cache last?
346  QString prevAddr;
347  const int index = text.lastIndexOf( QLatin1Char( ',' ) );
348  if ( index >= 0 ) {
349  prevAddr = text.left( index + 1 ) + QLatin1Char( ' ' );
350  text = text.mid( index + 1, 255 ).trimmed();
351  }
352 
353  if ( text.isEmpty() ) {
354  return;
355  }
356 
357  s_static->ldapSearch->startSearch( text );
358 }
359 
360 void AddresseeLineEdit::Private::stopLDAPLookup()
361 {
362  s_static->ldapSearch->cancelSearch();
363  s_static->ldapLineEdit = 0;
364 }
365 
366 
367 static const char* sparqlquery =
368  "select distinct ?email ?fullname where {"
369  "{"
370  "?r nco:hasEmailAddress ?em ."
371  "?em nco:emailAddress ?email ."
372  "?r ?p ?fullname ."
373  "FILTER( ?p in (nco:fullname, nco:nameFamily, nco:nameGiven, nco:nickname, nco:nameAdditional) )."
374  "FILTER( bif:contains(?fullname, \"'%1*'\") )."
375  "} UNION {"
376  "?r nco:hasEmailAddress ?em ."
377  "?em nco:emailAddress ?email ."
378  "FILTER( bif:contains(?email, \"'%1*'\") )."
379  "?r nco:fullname ?fullname ."
380  "}"
381  "}";
382 
383 void AddresseeLineEdit::Private::startNepomukSearch()
384 {
385  // We do a word boundary substring search, which will easily yield hundreds
386  // of hits. Since the nepomuk search is mostly an auxiliary measure,
387  // we limit it to substrings of size 4 (min. of bif:contains) or larger.
388  if ( m_searchString.size() <= 3 || !s_static->useNepomukCompletion ) {
389  return;
390  }
391  const QString query = QString::fromLatin1( sparqlquery ).arg( m_searchString );
392  Nepomuk2::Query::RequestPropertyMap requestPropertyMap;
393  requestPropertyMap.insert( QLatin1String("email"), Nepomuk2::Vocabulary::NCO::hasEmailAddress() );
394  requestPropertyMap.insert( QLatin1String("fullname"), Nepomuk2::Vocabulary::NCO::fullname() );
395  const bool result = s_static->nepomukSearchClient->sparqlQuery( query, requestPropertyMap );
396  if (!result) {
397  kDebug() << "Starting the nepomuk lookup failed. Search string: " << m_searchString;
398  }
399 }
400 
401 void AddresseeLineEdit::Private::stopNepomukSearch()
402 {
403  s_static->nepomukSearchClient->close();
404 }
405 
406 void AddresseeLineEdit::Private::slotNepomukHits( const QList<Nepomuk2::Query::Result>& results )
407 {
408  if ( results.isEmpty() || ( !q->hasFocus() && !q->completionBox()->hasFocus() ) ) {
409  return;
410  }
411  // Extract and process the hits
412  Q_FOREACH( const Nepomuk2::Query::Result& result, results) {
413  Soprano::Node node = result.requestProperty( Nepomuk2::Vocabulary::NCO::hasEmailAddress() );
414  if ( node.isValid() && node.isLiteral() ) {
415  const QString fullEmail = node.literal().toString();
416  Q_ASSERT(!fullEmail.isEmpty()); // the query took care of that, right?
417  QString formattedName;
418  // extract name, if we have one
419  Soprano::Node nodeName = result.requestProperty( Nepomuk2::Vocabulary::NCO::fullname() );
420  if ( nodeName.isValid() && nodeName.isLiteral() ) {
421  formattedName = nodeName.literal().toString();
422  }
423  Q_ASSERT(s_static);
424 
425  if ( formattedName.isEmpty() ) {
426  addCompletionItem( fullEmail, 1, s_static->nepomukCompletionSource );
427  } else {
428  const QString byFirstName = formattedName + QLatin1String(" <") + fullEmail + QLatin1Char('>');
429  addCompletionItem( byFirstName, 1, s_static->nepomukCompletionSource );
430  }
431  }
432  }
433 }
434 
435 void AddresseeLineEdit::Private::slotNepomukSearchFinished()
436 {
437  if ( q->hasFocus() || q->completionBox()->hasFocus() ) {
438  // only complete again if the user didn't change the selection while
439  // we were waiting; otherwise the completion box will be closed
440  const QListWidgetItem *current = q->completionBox()->currentItem();
441  if ( !current || m_searchString.trimmed() != current->text().trimmed() ) {
442  doCompletion( m_lastSearchMode );
443  }
444  }
445 }
446 
447 void AddresseeLineEdit::Private::setCompletedItems( const QStringList &items, bool autoSuggest )
448 {
449  KCompletionBox *completionBox = q->completionBox();
450 
451  if ( !items.isEmpty() &&
452  !( items.count() == 1 && m_searchString == items.first() ) ) {
453 
454  completionBox->clear();
455  const int numberOfItems( items.count() );
456  for ( int i = 0; i< numberOfItems; ++i )
457  {
458  QListWidgetItem *item =new QListWidgetItem(items.at( i ), completionBox);
459  if ( !items.at( i ).startsWith( s_completionItemIndentString ) )
460  item->setFlags( item->flags()&~Qt::ItemIsSelectable );
461  completionBox->addItem( item );
462  }
463  if ( !completionBox->isVisible() ) {
464  if ( !m_searchString.isEmpty() ) {
465  completionBox->setCancelledText( m_searchString );
466  }
467  completionBox->popup();
468  // we have to install the event filter after popup(), since that
469  // calls show(), and that's where KCompletionBox installs its filter.
470  // We want to be first, though, so do it now.
471  if ( s_static->completion->order() == KCompletion::Weighted ) {
472  qApp->installEventFilter( q );
473  }
474  }
475 
476  QListWidgetItem *item = completionBox->item( 1 );
477  if ( item ) {
478  completionBox->blockSignals( true );
479  completionBox->setCurrentItem( item );
480  item->setSelected( true );
481  completionBox->blockSignals( false );
482  }
483 
484  if ( autoSuggest ) {
485  const int index = items.first().indexOf( m_searchString );
486  const QString newText = items.first().mid( index );
487  q->setUserSelection( false );
488  q->setCompletedText( newText, true );
489  }
490  } else {
491  if ( completionBox && completionBox->isVisible() ) {
492  completionBox->hide();
493  completionBox->setItems( QStringList() );
494  }
495  }
496 }
497 
498 void AddresseeLineEdit::Private::addCompletionItem( const QString &string, int weight,
499  int completionItemSource,
500  const QStringList *keyWords )
501 {
502  // Check if there is an exact match for item already, and use the
503  // maximum weight if so. Since there's no way to get the information
504  // from KCompletion, we have to keep our own QMap.
505 
506  CompletionItemsMap::iterator it = s_static->completionItemMap.find( string );
507  if ( it != s_static->completionItemMap.end() ) {
508  weight = qMax( ( *it ).first, weight );
509  ( *it ).first = weight;
510  } else {
511  s_static->completionItemMap.insert( string, qMakePair( weight, completionItemSource ) );
512  }
513 
514  s_static->completion->addItem(string, weight);
515  if (keyWords && !keyWords->isEmpty())
516  s_static->completion->addItemWithKeys(string, weight, keyWords); // see kmailcompletion.cpp
517 }
518 
519 const QStringList KPIM::AddresseeLineEdit::Private::adjustedCompletionItems( bool fullSearch )
520 {
521  QStringList items = fullSearch ?
522  s_static->completion->allMatches( m_searchString ) :
523  s_static->completion->substringCompletion( m_searchString );
524 
525  //force items to be sorted by email
526  items.sort();
527 
528  // For weighted mode, the algorithm is the following:
529  // In the first loop, we add each item to its section (there is one section per completion source)
530  // We also add spaces in front of the items.
531  // The sections are appended to the items list.
532  // In the second loop, we then walk through the sections and add all the items in there to the
533  // sorted item list, which is the final result.
534  //
535  // The algo for non-weighted mode is different.
536 
537  int lastSourceIndex = -1;
538  unsigned int i = 0;
539 
540  // Maps indices of the items list, which are section headers/source items,
541  // to a QStringList which are the items of that section/source.
542  QMap<int, QStringList> sections;
543  QStringList sortedItems;
544  for ( QStringList::Iterator it = items.begin(); it != items.end(); ++it, ++i ) {
545  CompletionItemsMap::const_iterator cit = s_static->completionItemMap.constFind( *it );
546  if ( cit == s_static->completionItemMap.constEnd() ) {
547  continue;
548  }
549 
550  const int index = (*cit).second;
551 
552  if ( s_static->completion->order() == KCompletion::Weighted ) {
553  if ( lastSourceIndex == -1 || lastSourceIndex != index ) {
554  const QString sourceLabel( s_static->completionSources[ index ] );
555  if ( sections.find( index ) == sections.end() ) {
556  it = items.insert( it, sourceLabel );
557  ++it; //skip new item
558  }
559  lastSourceIndex = index;
560  }
561 
562  (*it) = (*it).prepend( s_completionItemIndentString );
563  // remove preferred email sort <blank> added in addContact()
564  (*it).replace( QLatin1String( " <" ), QLatin1String( " <" ) );
565  }
566  sections[ index ].append( *it );
567 
568  if ( s_static->completion->order() == KCompletion::Sorted ) {
569  sortedItems.append( *it );
570  }
571  }
572 
573  if ( s_static->completion->order() == KCompletion::Weighted ) {
574 
575  // Sort the sections
576  QList<SourceWithWeight> sourcesAndWeights;
577  const int numberOfCompletionSources(s_static->completionSources.size());
578  for ( int i = 0; i < numberOfCompletionSources; ++i ) {
579  SourceWithWeight sww;
580  sww.sourceName = s_static->completionSources[i];
581  sww.weight = s_static->completionSourceWeights[sww.sourceName];
582  sww.index = i;
583  sourcesAndWeights.append( sww );
584  }
585  qSort( sourcesAndWeights.begin(), sourcesAndWeights.end() );
586 
587  // Add the sections and their items to the final sortedItems result list
588  const int numberOfSources(sourcesAndWeights.size());
589  for ( int i = 0; i < numberOfSources; ++i ) {
590  const QStringList sectionItems = sections[sourcesAndWeights[i].index];
591  if ( !sectionItems.isEmpty() ) {
592  sortedItems.append( sourcesAndWeights[i].sourceName );
593  foreach ( const QString &itemInSection, sectionItems ) {
594  sortedItems.append( itemInSection );
595  }
596  }
597  }
598  } else {
599  sortedItems.sort();
600  }
601 
602  return sortedItems;
603 }
604 
605 void AddresseeLineEdit::Private::updateSearchString()
606 {
607  m_searchString = q->text();
608 
609  int n = -1;
610  bool inQuote = false;
611  uint searchStringLength = m_searchString.length();
612  for ( uint i = 0; i < searchStringLength; ++i ) {
613  const QChar searchChar = m_searchString[ i ];
614  if ( searchChar == QLatin1Char( '"' ) ) {
615  inQuote = !inQuote;
616  }
617 
618  if ( searchChar == QLatin1Char('\\') &&
619  ( i + 1 ) < searchStringLength && m_searchString[ i + 1 ] == QLatin1Char( '"' ) ) {
620  ++i;
621  }
622 
623  if ( inQuote ) {
624  continue;
625  }
626 
627  if ( i < searchStringLength &&
628  ( searchChar == QLatin1Char(',') ||
629  ( m_useSemicolonAsSeparator && searchChar == QLatin1Char(';') ) ) ) {
630  n = i;
631  }
632  }
633 
634  if ( n >= 0 ) {
635  ++n; // Go past the ","
636 
637  const int len = m_searchString.length();
638 
639  // Increment past any whitespace...
640  while ( n < len && m_searchString[ n ].isSpace() ) {
641  ++n;
642  }
643 
644  m_previousAddresses = m_searchString.left( n );
645  m_searchString = m_searchString.mid( n ).trimmed();
646  } else {
647  m_previousAddresses.clear();
648  }
649 }
650 
651 void AddresseeLineEdit::Private::slotTriggerDelayedQueries()
652 {
653  if (m_searchString.isEmpty())
654  return;
655 
656  // If Nepomuk is running, we send a ContactSearch job to
657  // Akonadi, which will forward the query to Nepomuk, be
658  // notified of matching items and return those to us.
659  akonadiPerformSearch();
660  // additionally, we ask Nepomuk directly, to get hits that
661  // did not come in through Akonadi
662  startNepomukSearch();
663 }
664 
665 void AddresseeLineEdit::Private::startSearches()
666 {
667  if ( Nepomuk2::ResourceManager::instance()->initialized() ) {
668  if (!m_delayedQueryTimer.isActive())
669  m_delayedQueryTimer.start(500);
670  } else {
671  // If Nepomuk is not available, we instead simply fetch
672  // all contacts from Akonadi and filter them ourselves.
673  // This is slower, but a reasonable fallback. We only do this
674  // once, since it returns the same contacts (all of them)
675  // every time. If one is added or removed during the lifetime
676  // of the widget, we miss that update, but that's better than
677  // doing useless repeat queries for every typed letter. We could
678  // monitor, but that doesn't seem worth the bother, for fallback
679  // code.
680  if ( !s_static->contactsListed ) {
681  akonadiListAllContacts();
682  s_static->contactsListed = true;
683  } else {
684  doCompletion( true );
685  }
686  }
687 }
688 
689 void AddresseeLineEdit::Private::akonadiPerformSearch()
690 {
691 
692  if ( m_searchString.size() <= 2 ) {
693  return;
694  }
695  kDebug() << "searching akonadi with:" << m_searchString;
696 
697  // first, kill all job still in flight, they are no longer current
698  Q_FOREACH( QWeakPointer<Akonadi::Job> job, s_static->akonadiJobsInFlight ) {
699  if ( !job.isNull() ) {
700  job.data()->kill();
701  }
702  }
703  s_static->akonadiJobsInFlight.clear();
704 
705  // now start new jobs
706  Akonadi::ContactSearchJob *contactJob = new Akonadi::ContactSearchJob( s_static->akonadiSession );
707  Akonadi::ContactGroupSearchJob *groupJob = new Akonadi::ContactGroupSearchJob( s_static->akonadiSession );
708  contactJob->fetchScope().setAncestorRetrieval( Akonadi::ItemFetchScope::Parent );
709  groupJob->fetchScope().setAncestorRetrieval( Akonadi::ItemFetchScope::Parent );
710  contactJob->setQuery( Akonadi::ContactSearchJob::NameOrEmail, m_searchString,
711  Akonadi::ContactSearchJob::ContainsWordBoundaryMatch );
712  groupJob->setQuery( Akonadi::ContactGroupSearchJob::Name, m_searchString,
713  Akonadi::ContactGroupSearchJob::StartsWithMatch );
714  // FIXME: ContainsMatch is broken, even though it creates the correct SPARQL query, so use
715  // StartsWith for now
716  //Akonadi::ContactGroupSearchJob::ContainsMatch );
717  q->connect( contactJob, SIGNAL(result(KJob*)),
718  q, SLOT(slotAkonadiSearchResult(KJob*)) );
719  q->connect( groupJob, SIGNAL(result(KJob*)),
720  q, SLOT(slotAkonadiSearchResult(KJob*)) );
721  s_static->akonadiJobsInFlight.append( contactJob );
722  s_static->akonadiJobsInFlight.append( groupJob );
723  akonadiHandlePending();
724 }
725 
726 void AddresseeLineEdit::Private::akonadiListAllContacts()
727 {
728  kDebug() << "listing all contacts in Akonadi";
729  Akonadi::RecursiveItemFetchJob *job =
730  new Akonadi::RecursiveItemFetchJob( Akonadi::Collection::root(),
731  QStringList() << KABC::Addressee::mimeType() << KABC::ContactGroup::mimeType(),
732  s_static->akonadiSession );
733  job->fetchScope().fetchFullPayload();
734  job->fetchScope().setAncestorRetrieval( Akonadi::ItemFetchScope::Parent );
735  q->connect( job, SIGNAL(result(KJob*)),
736  q, SLOT(slotAkonadiSearchDbResult(KJob*)) );
737  job->start();
738  akonadiHandlePending();
739 }
740 
741 void AddresseeLineEdit::Private::akonadiHandlePending()
742 {
743  kDebug() << "Pending items: " << s_static->akonadiPendingItems.size();
744  Akonadi::Item::List::iterator it = s_static->akonadiPendingItems.begin();
745  while ( it != s_static->akonadiPendingItems.end() ) {
746  const Akonadi::Item item = *it;
747 
748  const int sourceIndex =
749  s_static->akonadiCollectionToCompletionSourceMap.value( item.parentCollection().id(), -1 );
750  if ( sourceIndex >= 0 ) {
751  kDebug() << "identified collection: " << s_static->completionSources[sourceIndex];
752  q->addItem( item, 1, sourceIndex );
753 
754  // remove from the pending
755  it = s_static->akonadiPendingItems.erase( it );
756  } else {
757  ++it;
758  }
759  }
760 }
761 
762 void AddresseeLineEdit::Private::doCompletion( bool ctrlT )
763 {
764  m_lastSearchMode = ctrlT;
765 
766  const KGlobalSettings::Completion mode = q->completionMode();
767 
768  if ( mode == KGlobalSettings::CompletionNone ) {
769  return;
770  }
771 
772  s_static->completion->setOrder( completionOrder() );
773 
774  // cursor at end of string - or Ctrl+T pressed for substring completion?
775  if ( ctrlT ) {
776  const QStringList completions = adjustedCompletionItems( false );
777 
778  if ( completions.count() > 1 ) {
779  ; //m_previousAddresses = prevAddr;
780  } else if ( completions.count() == 1 ) {
781  q->setText( m_previousAddresses + completions.first().trimmed() );
782  }
783 
784  // Make sure the completion popup is closed if no matching items were found
785  setCompletedItems( completions, true );
786 
787  q->cursorAtEnd();
788  q->setCompletionMode( mode ); //set back to previous mode
789  return;
790  }
791 
792  switch ( mode ) {
793  case KGlobalSettings::CompletionPopupAuto:
794  {
795  if ( m_searchString.isEmpty() ) {
796  break;
797  }
798  //else: fall-through to the CompletionPopup case
799  }
800 
801  case KGlobalSettings::CompletionPopup:
802  {
803  const QStringList items = adjustedCompletionItems( false );
804  setCompletedItems( items, false );
805  }
806  break;
807 
808  case KGlobalSettings::CompletionShell:
809  {
810  const QString match = s_static->completion->makeCompletion( m_searchString );
811  if ( !match.isNull() && match != m_searchString ) {
812  q->setText( m_previousAddresses + match );
813  q->setModified( true );
814  q->cursorAtEnd();
815  }
816  }
817  break;
818 
819  case KGlobalSettings::CompletionMan: // Short-Auto in fact
820  case KGlobalSettings::CompletionAuto:
821  {
822  //force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect
823  q->setCompletionMode( q->completionMode() );
824 
825  if ( !m_searchString.isEmpty() ) {
826 
827  //if only our \" is left, remove it since user has not typed it either
828  if ( m_searchExtended && m_searchString == QLatin1String( "\"" ) ) {
829  m_searchExtended = false;
830  m_searchString.clear();
831  q->setText( m_previousAddresses );
832  break;
833  }
834 
835  QString match = s_static->completion->makeCompletion( m_searchString );
836 
837  if ( !match.isEmpty() ) {
838  if ( match != m_searchString ) {
839  QString adds = m_previousAddresses + match;
840  q->setCompletedText( adds );
841  }
842  } else {
843  if ( !m_searchString.startsWith( QLatin1Char( '\"' ) ) ) {
844  //try with quoted text, if user has not type one already
845  match = s_static->completion->makeCompletion( QLatin1String( "\"" ) + m_searchString );
846  if ( !match.isEmpty() && match != m_searchString ) {
847  m_searchString = QLatin1String( "\"" ) + m_searchString;
848  m_searchExtended = true;
849  q->setText( m_previousAddresses + m_searchString );
850  q->setCompletedText( m_previousAddresses + match );
851  }
852  } else if ( m_searchExtended ) {
853  //our added \" does not work anymore, remove it
854  m_searchString = m_searchString.mid( 1 );
855  m_searchExtended = false;
856  q->setText( m_previousAddresses + m_searchString );
857  //now try again
858  match = s_static->completion->makeCompletion( m_searchString );
859  if ( !match.isEmpty() && match != m_searchString ) {
860  const QString adds = m_previousAddresses + match;
861  q->setCompletedText( adds );
862  }
863  }
864  }
865  }
866  }
867  break;
868 
869  case KGlobalSettings::CompletionNone:
870  default: // fall through
871  break;
872  }
873 }
874 
875 void AddresseeLineEdit::Private::slotCompletion()
876 {
877  // Called by KLineEdit's keyPressEvent for CompletionModes
878  // Auto,Popup -> new text, update search string.
879  // not called for CompletionShell, this is been taken care of
880  // in AddresseeLineEdit::keyPressEvent
881 
882  updateSearchString();
883  if ( q->completionBox() ) {
884  q->completionBox()->setCancelledText( m_searchString );
885  }
886 
887  startSearches();
888  doCompletion( false );
889 }
890 
891 void AddresseeLineEdit::Private::slotPopupCompletion( const QString &completion )
892 {
893  q->setText( m_previousAddresses + completion.trimmed() );
894  q->cursorAtEnd();
895  updateSearchString();
896  q->emitTextCompleted();
897 }
898 
899 void AddresseeLineEdit::Private::slotReturnPressed( const QString & )
900 {
901  if ( !q->completionBox()->selectedItems().isEmpty() ) {
902  slotPopupCompletion( q->completionBox()->selectedItems().first()->text() );
903  }
904 }
905 
906 void AddresseeLineEdit::Private::slotStartLDAPLookup()
907 {
908  if ( Solid::Networking::status() == Solid::Networking::Unconnected ) {
909  return;
910  }
911 
912  const KGlobalSettings::Completion mode = q->completionMode();
913 
914  if ( mode == KGlobalSettings::CompletionNone ) {
915  return;
916  }
917 
918  if ( !s_static->ldapSearch->isAvailable() ) {
919  return;
920  }
921 
922  if ( s_static->ldapLineEdit != q ) {
923  return;
924  }
925 
926  startLoadingLDAPEntries();
927 }
928 
929 void AddresseeLineEdit::Private::slotLDAPSearchData( const KLDAP::LdapResult::List &results )
930 {
931  if ( results.isEmpty() || s_static->ldapLineEdit != q ) {
932  return;
933  }
934 
935  foreach ( const KLDAP::LdapResult &result, results ) {
936  KABC::Addressee contact;
937  contact.setNameFromString( result.name );
938  contact.setEmails( result.email );
939 
940  if ( !s_static->ldapClientToCompletionSourceMap.contains( result.clientNumber ) ) {
941  s_static->updateLDAPWeights(); // we got results from a new source, so update the completion sources
942  }
943 
944  q->addContact( contact, result.completionWeight,
945  s_static->ldapClientToCompletionSourceMap[ result.clientNumber ] );
946  }
947 
948  if ( ( q->hasFocus() || q->completionBox()->hasFocus() ) &&
949  q->completionMode() != KGlobalSettings::CompletionNone &&
950  q->completionMode() != KGlobalSettings::CompletionShell ) {
951  q->setText( m_previousAddresses + m_searchString );
952  // only complete again if the user didn't change the selection while
953  // we were waiting; otherwise the completion box will be closed
954  const QListWidgetItem *current = q->completionBox()->currentItem();
955  if ( !current || m_searchString.trimmed() != current->text().trimmed() ) {
956  doCompletion( m_lastSearchMode );
957  }
958  }
959 }
960 
961 void AddresseeLineEdit::Private::slotEditCompletionOrder()
962 {
963  init(); // for s_static->ldapSearch
964 #ifndef Q_OS_WINCE
965  if(m_useCompletion){
966  s_static->slotEditCompletionOrder();
967  }
968 #endif
969 }
970 
971 void AddresseeLineEdit::Private::slotUserCancelled( const QString &cancelText )
972 {
973  if ( s_static->ldapSearch && s_static->ldapLineEdit == q ) {
974  stopLDAPLookup();
975  }
976 
977  if ( s_static->nepomukSearchClient ) {
978  stopNepomukSearch();
979  }
980  q->userCancelled( m_previousAddresses + cancelText ); // in KLineEdit
981 }
982 
983 void AddresseeLineEdit::Private::akonadiHandleItems( const Akonadi::Item::List &items )
984 {
985  /* We have to fetch the collections of the items, so that
986  the source name can be correctly labeled.*/
987  foreach ( const Akonadi::Item &item, items ) {
988 
989  // check the local cache of collections
990  const int sourceIndex =
991  s_static->akonadiCollectionToCompletionSourceMap.value( item.parentCollection().id(), -1 );
992  if ( sourceIndex == -1 ) {
993  kDebug() << "Fetching New collection: " << item.parentCollection().id();
994  // the collection isn't there, start the fetch job.
995  Akonadi::CollectionFetchJob *collectionJob =
996  new Akonadi::CollectionFetchJob( item.parentCollection(),
997  Akonadi::CollectionFetchJob::Base,
998  s_static->akonadiSession );
999  connect( collectionJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
1000  q, SLOT(slotAkonadiCollectionsReceived(Akonadi::Collection::List)) );
1001  /* we don't want to start multiple fetch jobs for the same collection,
1002  so insert the collection with an index value of -2 */
1003  s_static->akonadiCollectionToCompletionSourceMap.insert( item.parentCollection().id(), -2 );
1004  s_static->akonadiPendingItems.append( item );
1005  } else if ( sourceIndex == -2 ) {
1006  /* fetch job already started, don't need to start another one,
1007  so just append the item as pending */
1008  s_static->akonadiPendingItems.append( item );
1009  } else {
1010  q->addItem( item, 1, sourceIndex );
1011  }
1012  }
1013 
1014  if ( !items.isEmpty() ) {
1015  const QListWidgetItem *current = q->completionBox()->currentItem();
1016  if ( !current || m_searchString.trimmed() != current->text().trimmed() ) {
1017  doCompletion( m_lastSearchMode );
1018  }
1019  }
1020 }
1021 
1022 void AddresseeLineEdit::Private::slotAkonadiSearchResult( KJob *job )
1023 {
1024  const int index = s_static->akonadiJobsInFlight.indexOf( qobject_cast<Akonadi::Job*>( job ) );
1025  if( index != -1 )
1026  s_static->akonadiJobsInFlight.remove( index );
1027  const Akonadi::ContactSearchJob *contactJob =
1028  qobject_cast<Akonadi::ContactSearchJob*>( job );
1029  const Akonadi::ContactGroupSearchJob *groupJob =
1030  qobject_cast<Akonadi::ContactGroupSearchJob*>( job );
1031 
1032  Akonadi::Item::List items;
1033  if ( contactJob ) {
1034  items += contactJob->items();
1035  kDebug() << "Found" << contactJob->contacts().size() << "contacts";
1036  } else if ( groupJob ) {
1037  items += groupJob->items();
1038  kDebug() << "Found" << groupJob->contactGroups().size() << "groups";
1039  }
1040  akonadiHandleItems( items );
1041 }
1042 
1043 void AddresseeLineEdit::Private::slotAkonadiSearchDbResult( KJob *job )
1044 {
1045  const Akonadi::RecursiveItemFetchJob *contactJob =
1046  qobject_cast<Akonadi::RecursiveItemFetchJob*>( job );
1047  Akonadi::Item::List items;
1048  if ( contactJob ) {
1049  items += contactJob->items();
1050  kDebug() << "Found in DB directly:" << contactJob->items().size() << "contacts";
1051  }
1052  akonadiHandleItems( items );
1053 }
1054 
1055 void AddresseeLineEdit::Private::slotAkonadiCollectionsReceived(
1056  const Akonadi::Collection::List &collections )
1057 {
1058  foreach ( const Akonadi::Collection &collection, collections ) {
1059  if ( collection.isValid() ) {
1060  const QString sourceString = collection.displayName();
1061  const int index = q->addCompletionSource( sourceString, 1 );
1062  kDebug() << "\treceived: " << sourceString << "index: " << index;
1063  s_static->akonadiCollectionToCompletionSourceMap.insert( collection.id(), index );
1064  }
1065  }
1066 
1067  // now that we have added the new collections, recheck our list of pending contacts
1068  akonadiHandlePending();
1069  // do completion
1070  const QListWidgetItem *current = q->completionBox()->currentItem();
1071  if ( !current || m_searchString.trimmed() != current->text().trimmed() ) {
1072  doCompletion( m_lastSearchMode );
1073  }
1074 }
1075 
1076 // not cached, to make sure we get an up-to-date value when it changes
1077 KCompletion::CompOrder AddresseeLineEdit::Private::completionOrder()
1078 {
1079  KConfig _config( QLatin1String( "kpimcompletionorder" ) );
1080  const KConfigGroup config( &_config, QLatin1String( "General" ) );
1081  const QString order =
1082  config.readEntry( QLatin1String( "CompletionOrder" ), QString::fromLatin1( "Weighted" ) );
1083 
1084  if ( order == QLatin1String( "Weighted" ) ) {
1085  return KCompletion::Weighted;
1086  } else {
1087  return KCompletion::Sorted;
1088  }
1089 }
1090 
1091 AddresseeLineEdit::AddresseeLineEdit( QWidget *parent, bool enableCompletion )
1092  : KLineEdit( parent ), d( new Private( this, enableCompletion ) )
1093 {
1094  setObjectName( newLineEditObjectName() );
1095  setClickMessage( QString() );
1096 
1097  d->init();
1098 }
1099 
1100 AddresseeLineEdit::~AddresseeLineEdit()
1101 {
1102  if ( s_static->ldapSearch && s_static->ldapLineEdit == this ) {
1103  d->stopLDAPLookup();
1104  }
1105  if( s_static->nepomukSearchClient ) {
1106  d->stopNepomukSearch();
1107  }
1108  delete d;
1109 }
1110 
1111 void AddresseeLineEdit::setFont( const QFont &font )
1112 {
1113  KLineEdit::setFont( font );
1114 
1115  if ( d->m_useCompletion ) {
1116  completionBox()->setFont( font );
1117  }
1118 }
1119 
1120 void AddresseeLineEdit::allowSemicolonAsSeparator( bool useSemicolonAsSeparator )
1121 {
1122  d->m_useSemicolonAsSeparator = useSemicolonAsSeparator;
1123 }
1124 
1125 void AddresseeLineEdit::keyPressEvent( QKeyEvent *event )
1126 {
1127  bool accept = false;
1128 
1129  const int key = event->key() | event->modifiers();
1130 
1131  if ( KStandardShortcut::shortcut( KStandardShortcut::SubstringCompletion ).contains( key ) ) {
1132  //TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch
1133  d->updateSearchString();
1134  d->startSearches();
1135  d->doCompletion( true );
1136  accept = true;
1137  } else if ( KStandardShortcut::shortcut( KStandardShortcut::TextCompletion ).contains( key ) ) {
1138  const int len = text().length();
1139 
1140  if ( len == cursorPosition() ) { // at End?
1141  d->updateSearchString();
1142  d->startSearches();
1143  d->doCompletion( true );
1144  accept = true;
1145  }
1146  }
1147 
1148  const QString oldContent = text();
1149  if ( !accept ) {
1150  KLineEdit::keyPressEvent( event );
1151  }
1152 
1153  // if the text didn't change (eg. because a cursor navigation key was pressed)
1154  // we don't need to trigger a new search
1155  if ( oldContent == text() ) {
1156  return;
1157  }
1158 
1159  if ( event->isAccepted() ) {
1160  d->updateSearchString();
1161 
1162  QString searchString( d->m_searchString );
1163  //LDAP does not know about our string manipulation, remove it
1164  if ( d->m_searchExtended ) {
1165  searchString = d->m_searchString.mid( 1 );
1166  }
1167 
1168  if ( d->m_useCompletion && s_static->ldapTimer ) {
1169  if ( s_static->ldapText != searchString || s_static->ldapLineEdit != this ) {
1170  d->stopLDAPLookup();
1171  }
1172 
1173  s_static->ldapText = searchString;
1174  s_static->ldapLineEdit = this;
1175  s_static->ldapTimer->setSingleShot( true );
1176  s_static->ldapTimer->start( 500 );
1177  }
1178  }
1179 }
1180 
1181 void AddresseeLineEdit::insert( const QString &t )
1182 {
1183  if ( !d->m_smartPaste ) {
1184  KLineEdit::insert( t );
1185  return;
1186  }
1187 
1188  QString newText = t.trimmed();
1189  if ( newText.isEmpty() ) {
1190  return;
1191  }
1192 
1193  // remove newlines in the to-be-pasted string
1194  QStringList lines = newText.split( QRegExp( QLatin1String( "\r?\n" ) ), QString::SkipEmptyParts );
1195  QStringList::iterator end( lines.end() );
1196  for ( QStringList::iterator it = lines.begin(); it != end; ++it ) {
1197  // remove trailing commas and whitespace
1198  (*it).remove( QRegExp( QLatin1String( ",?\\s*$" ) ) );
1199  }
1200  newText = lines.join( QLatin1String( ", " ) );
1201 
1202  if ( newText.startsWith( QLatin1String( "mailto:" ) ) ) {
1203  const KUrl url( newText );
1204  newText = url.path();
1205  } else if ( newText.indexOf( QLatin1String( " at " ) ) != -1 ) {
1206  // Anti-spam stuff
1207  newText.replace( QLatin1String( " at " ), QLatin1String( "@" ) );
1208  newText.replace( QLatin1String( " dot " ), QLatin1String( "." ) );
1209  } else if ( newText.indexOf( QLatin1String( "(at)" ) ) != -1 ) {
1210  newText.replace( QRegExp( QLatin1String( "\\s*\\(at\\)\\s*" ) ), QLatin1String( "@" ) );
1211  }
1212 
1213  QString contents = text();
1214  int start_sel = 0;
1215  int pos = cursorPosition();
1216 
1217  if ( hasSelectedText() ) {
1218  // Cut away the selection.
1219  start_sel = selectionStart();
1220  pos = start_sel;
1221  contents = contents.left( start_sel ) + contents.mid( start_sel + selectedText().length() );
1222  }
1223 
1224  int eot = contents.length();
1225  while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() ) {
1226  eot--;
1227  }
1228  if ( eot == 0 ) {
1229  contents.clear();
1230  } else if ( pos >= eot ) {
1231  if ( contents[ eot - 1 ] == QLatin1Char( ',' ) ) {
1232  eot--;
1233  }
1234  contents.truncate( eot );
1235  contents += QLatin1String( ", " );
1236  pos = eot + 2;
1237  }
1238 
1239  contents = contents.left( pos ) + newText + contents.mid( pos );
1240  setText( contents );
1241  setModified( true );
1242  setCursorPosition( pos + newText.length() );
1243 }
1244 
1245 void AddresseeLineEdit::setText( const QString & text )
1246 {
1247  const int cursorPos = cursorPosition();
1248  KLineEdit::setText( text.trimmed() );
1249  setCursorPosition( cursorPos );
1250 }
1251 
1252 void AddresseeLineEdit::paste()
1253 {
1254  if ( d->m_useCompletion ) {
1255  d->m_smartPaste = true;
1256  }
1257 
1258  KLineEdit::paste();
1259  d->m_smartPaste = false;
1260 }
1261 
1262 void AddresseeLineEdit::mouseReleaseEvent( QMouseEvent *event )
1263 {
1264  // reimplemented from QLineEdit::mouseReleaseEvent()
1265 #ifndef QT_NO_CLIPBOARD
1266  if ( d->m_useCompletion &&
1267  QApplication::clipboard()->supportsSelection() &&
1268  !isReadOnly() &&
1269  event->button() == Qt::MidButton ) {
1270  d->m_smartPaste = true;
1271  }
1272 #endif
1273 
1274  KLineEdit::mouseReleaseEvent( event );
1275  d->m_smartPaste = false;
1276 }
1277 
1278 #ifndef QT_NO_DRAGANDDROP
1279 void AddresseeLineEdit::dropEvent( QDropEvent *event )
1280 {
1281  if ( !isReadOnly() ) {
1282  const KUrl::List uriList = KUrl::List::fromMimeData( event->mimeData() );
1283  if ( !uriList.isEmpty() ) {
1284  QString contents = text();
1285  // remove trailing white space and comma
1286  int eot = contents.length();
1287  while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() ) {
1288  eot--;
1289  }
1290  if ( eot == 0 ) {
1291  contents.clear();
1292  } else if ( contents[ eot - 1 ] == QLatin1Char(',') ) {
1293  eot--;
1294  contents.truncate( eot );
1295  }
1296  bool mailtoURL = false;
1297  // append the mailto URLs
1298  foreach ( const KUrl &url, uriList ) {
1299  if ( url.protocol() == QLatin1String( "mailto" ) ) {
1300  mailtoURL = true;
1301  QString address;
1302  address = KUrl::fromPercentEncoding( url.path().toLatin1() );
1303  address = KMime::decodeRFC2047String( address.toLatin1() );
1304  if ( !contents.isEmpty() ) {
1305  contents.append( QLatin1String( ", " ) );
1306  }
1307  contents.append( address );
1308  }
1309  }
1310  if ( mailtoURL ) {
1311  setText( contents );
1312  setModified( true );
1313  return;
1314  }
1315  } else {
1316  // Let's see if this drop contains a comma separated list of emails
1317  const QString dropData = QString::fromUtf8( event->encodedData( "text/plain" ) );
1318  const QStringList addrs = KPIMUtils::splitAddressList( dropData );
1319  if ( !addrs.isEmpty() ) {
1320  setText( KPIMUtils::normalizeAddressesAndDecodeIdn( dropData ) );
1321  setModified( true );
1322  return;
1323  }
1324  }
1325  }
1326 
1327  if ( d->m_useCompletion ) {
1328  d->m_smartPaste = true;
1329  }
1330 
1331  QLineEdit::dropEvent( event );
1332  d->m_smartPaste = false;
1333 }
1334 #endif // QT_NO_DRAGANDDROP
1335 
1336 void AddresseeLineEdit::cursorAtEnd()
1337 {
1338  setCursorPosition( text().length() );
1339 }
1340 
1341 void AddresseeLineEdit::enableCompletion( bool enable )
1342 {
1343  d->m_useCompletion = enable;
1344 }
1345 
1346 void AddresseeLineEdit::addItem( const Akonadi::Item &item, int weight, int source )
1347 {
1348  if ( item.hasPayload<KABC::Addressee>() ) {
1349  addContact( item.payload<KABC::Addressee>(), weight, source );
1350  } else if ( item.hasPayload<KABC::ContactGroup>() ) {
1351  addContactGroup( item.payload<KABC::ContactGroup>(), weight, source );
1352  }
1353 }
1354 
1355 void AddresseeLineEdit::addContactGroup( const KABC::ContactGroup &group, int weight, int source )
1356 {
1357  d->addCompletionItem( group.name(), weight, source );
1358 }
1359 
1360 void AddresseeLineEdit::addContact( const KABC::Addressee &addr, int weight, int source )
1361 {
1362  const QStringList emails = addr.emails();
1363  QStringList::ConstIterator it;
1364  int isPrefEmail = 1; //first in list is preferredEmail
1365  QStringList::ConstIterator end( emails.constEnd() );
1366  for ( it = emails.constBegin(); it != end; ++it ) {
1367  //TODO: highlight preferredEmail
1368  const QString email( (*it) );
1369  const QString givenName = addr.givenName();
1370  const QString familyName= addr.familyName();
1371  const QString nickName = addr.nickName();
1372  QString fullEmail = addr.fullEmail( email );
1373 
1374  // Prepare "givenName" + ' ' + "familyName"
1375  QString fullName = givenName;
1376  if (!familyName.isEmpty()) {
1377  if (!fullName.isEmpty())
1378  fullName += QLatin1Char(' ');
1379  fullName += familyName;
1380  }
1381 
1382  // Finally, we can add the completion items
1383  if (!fullName.isEmpty()) {
1384  const QString address = KPIMUtils::normalizedAddress(fullName, email, QString());
1385  if (fullEmail != address) {
1386  // This happens when fullEmail contains a middle name, while our own fullName+email only has "first last".
1387  // Let's offer both, the fullEmail with 3 parts, looks a tad formal.
1388  d->addCompletionItem(address, weight + isPrefEmail, source);
1389  }
1390  }
1391 
1392  QStringList keyWords;
1393  if (!nickName.isEmpty()) {
1394  keyWords.append(nickName);
1395  }
1396 
1397  d->addCompletionItem( fullEmail, weight + isPrefEmail, source, &keyWords );
1398 
1399  isPrefEmail = 0;
1400  }
1401 }
1402 
1403 #ifndef QT_NO_CONTEXTMENU
1404 void AddresseeLineEdit::contextMenuEvent( QContextMenuEvent *event )
1405 {
1406  QMenu *menu = createStandardContextMenu();
1407  if ( menu ) { // can be 0 on platforms with only a touch interface
1408  menu->exec( event->globalPos() );
1409  delete menu;
1410  }
1411 }
1412 
1413 QMenu *AddresseeLineEdit::createStandardContextMenu()
1414 {
1415  // disable modes not supported by KMailCompletion
1416  setCompletionModeDisabled( KGlobalSettings::CompletionMan );
1417  setCompletionModeDisabled( KGlobalSettings::CompletionPopupAuto );
1418 
1419  QMenu *menu = KLineEdit::createStandardContextMenu();
1420  if ( !menu ) {
1421  return 0;
1422  }
1423 
1424  if ( d->m_useCompletion ) {
1425  menu->addAction( i18n( "Configure Completion Order..." ),
1426  this, SLOT(slotEditCompletionOrder()) );
1427  }
1428  return menu;
1429 }
1430 #endif
1431 
1432 void KPIM::AddresseeLineEdit::removeCompletionSource(const QString &source)
1433 {
1434  s_static->removeCompletionSource(source);
1435 }
1436 
1437 int KPIM::AddresseeLineEdit::addCompletionSource( const QString &source, int weight )
1438 {
1439  return s_static->addCompletionSource(source,weight);
1440 }
1441 
1442 bool KPIM::AddresseeLineEdit::eventFilter( QObject *object, QEvent *event )
1443 {
1444  if ( d->m_completionInitialized &&
1445  ( object == completionBox() ||
1446  completionBox()->findChild<QWidget*>( object->objectName() ) == object ) ) {
1447  if ( event->type() == QEvent::MouseButtonPress ||
1448  event->type() == QEvent::MouseMove ||
1449  event->type() == QEvent::MouseButtonRelease ||
1450  event->type() == QEvent::MouseButtonDblClick ) {
1451 
1452  const QMouseEvent* mouseEvent = static_cast<QMouseEvent*>( event );
1453  // find list box item at the event position
1454  QListWidgetItem *item = completionBox()->itemAt( mouseEvent->pos() );
1455  if ( !item ) {
1456  // In the case of a mouse move outside of the box we don't want
1457  // the parent to fuzzy select a header by mistake.
1458  bool eat = event->type() == QEvent::MouseMove;
1459  return eat;
1460  }
1461  // avoid selection of headers on button press, or move or release while
1462  // a button is pressed
1463  const Qt::MouseButtons buttons = mouseEvent->buttons();
1464  if ( event->type() == QEvent::MouseButtonPress ||
1465  event->type() == QEvent::MouseButtonDblClick ||
1466  buttons & Qt::LeftButton || buttons & Qt::MidButton ||
1467  buttons & Qt::RightButton ) {
1468  if ( itemIsHeader( item ) ) {
1469  return true; // eat the event, we don't want anything to happen
1470  } else {
1471  // if we are not on one of the group heading, make sure the item
1472  // below or above is selected, not the heading, inadvertedly, due
1473  // to fuzzy auto-selection from QListBox
1474  completionBox()->setCurrentItem( item );
1475  item->setSelected( true );
1476  if ( event->type() == QEvent::MouseMove ) {
1477  return true; // avoid fuzzy selection behavior
1478  }
1479  }
1480  }
1481  }
1482  }
1483 
1484  if ( ( object == this ) &&
1485  ( event->type() == QEvent::ShortcutOverride ) ) {
1486  QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
1487  if ( keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down ||
1488  keyEvent->key() == Qt::Key_Tab ) {
1489  keyEvent->accept();
1490  return true;
1491  }
1492  }
1493 
1494  if ( ( object == this ) &&
1495  ( event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease ) &&
1496  completionBox()->isVisible() ) {
1497  const QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
1498  int currentIndex = completionBox()->currentRow();
1499  if ( currentIndex < 0 ) {
1500  return true;
1501  }
1502  if ( keyEvent->key() == Qt::Key_Up ) {
1503  //kDebug() <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex;
1504  // figure out if the item we would be moving to is one we want
1505  // to ignore. If so, go one further
1506  const QListWidgetItem *itemAbove = completionBox()->item( currentIndex );
1507  if ( itemAbove && itemIsHeader( itemAbove ) ) {
1508  // there is a header above is, check if there is even further up
1509  // and if so go one up, so it'll be selected
1510  if ( currentIndex > 0 && completionBox()->item( currentIndex - 1 ) ) {
1511  //kDebug() <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1;
1512  completionBox()->setCurrentRow( currentIndex - 1 );
1513  completionBox()->item( currentIndex - 1 )->setSelected( true );
1514  } else if ( currentIndex == 0 ) {
1515  // nothing to skip to, let's stay where we are, but make sure the
1516  // first header becomes visible, if we are the first real entry
1517  completionBox()->scrollToItem( completionBox()->item( 0 ) );
1518  QListWidgetItem *item = completionBox()->item( currentIndex );
1519  if ( item ) {
1520  if ( itemIsHeader( item ) ) {
1521  currentIndex++;
1522  item = completionBox()->item( currentIndex );
1523  }
1524  completionBox()->setCurrentItem( item );
1525  item->setSelected( true );
1526  }
1527  }
1528 
1529  return true;
1530  }
1531  } else if ( keyEvent->key() == Qt::Key_Down ) {
1532  // same strategy for downwards
1533  //kDebug() <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex;
1534  const QListWidgetItem *itemBelow = completionBox()->item( currentIndex );
1535  if ( itemBelow && itemIsHeader( itemBelow ) ) {
1536  if ( completionBox()->item( currentIndex + 1 ) ) {
1537  //kDebug() <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1;
1538  completionBox()->setCurrentRow( currentIndex + 1 );
1539  completionBox()->item( currentIndex + 1 )->setSelected( true );
1540  } else {
1541  // nothing to skip to, let's stay where we are
1542  QListWidgetItem *item = completionBox()->item( currentIndex );
1543  if ( item ) {
1544  completionBox()->setCurrentItem( item );
1545  item->setSelected( true );
1546  }
1547  }
1548 
1549  return true;
1550  }
1551  // special case of the initial selection, which is unfortunately a header.
1552  // Setting it to selected tricks KCompletionBox into not treating is special
1553  // and selecting making it current, instead of the one below.
1554  QListWidgetItem *item = completionBox()->item( currentIndex );
1555  if ( item && itemIsHeader( item ) ) {
1556  completionBox()->setCurrentItem( item );
1557  item->setSelected( true );
1558  }
1559  } else if ( event->type() == QEvent::KeyRelease &&
1560  ( keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab ) ) {
1562  QListWidgetItem *myHeader = 0;
1563  int myHeaderIndex = -1;
1564  const int iterationStep = keyEvent->key() == Qt::Key_Tab ? 1 : -1;
1565  int index = qMin( qMax( currentIndex - iterationStep, 0 ), completionBox()->count() - 1 );
1566  while ( index >= 0 ) {
1567  if ( itemIsHeader( completionBox()->item( index ) ) ) {
1568  myHeader = completionBox()->item( index );
1569  myHeaderIndex = index;
1570  break;
1571  }
1572 
1573  index--;
1574  }
1575  Q_ASSERT( myHeader ); // we should always be able to find a header
1576 
1577  // find the next header (searching backwards, for Qt::Key_Backtab)
1578  QListWidgetItem *nextHeader = 0;
1579 
1580  // when iterating forward, start at the currentindex, when backwards,
1581  // one up from our header, or at the end
1582  uint j;
1583  if ( keyEvent->key() == Qt::Key_Tab ) {
1584  j = currentIndex;
1585  } else {
1586  index = myHeaderIndex;
1587  if ( index == 0 ) {
1588  j = completionBox()->count() - 1;
1589  } else {
1590  j = ( index - 1 ) % completionBox()->count();
1591  }
1592  }
1593  while ( ( nextHeader = completionBox()->item( j ) ) && nextHeader != myHeader ) {
1594  if ( itemIsHeader( nextHeader ) ) {
1595  break;
1596  }
1597  j = ( j + iterationStep ) % completionBox()->count();
1598  }
1599 
1600  if ( nextHeader && nextHeader != myHeader ) {
1601  QListWidgetItem *item = completionBox()->item( j + 1 );
1602  if ( item && !itemIsHeader( item ) ) {
1603  completionBox()->setCurrentItem( item );
1604  item->setSelected( true );
1605  }
1606  }
1607 
1608  return true;
1609  }
1610  }
1611 
1612  return KLineEdit::eventFilter( object, event );
1613 }
1614 
1615 void AddresseeLineEdit::emitTextCompleted()
1616 {
1617  emit textCompleted();
1618 }
1619 
1620 
1621 
1622 
1623 #include "addresseelineedit.moc"
sparqlquery
static const char * sparqlquery
Definition: addresseelineedit.cpp:367
KLDAP::LdapClient::server
const KLDAP::LdapServer server() const
Returns the ldap server information that are used by this client.
Definition: ldapclient.cpp:120
KPIM::CompletionOrderEditor
Definition: completionordereditor.h:65
KPIM::AddresseeLineEdit::contextMenuEvent
virtual void contextMenuEvent(QContextMenuEvent *)
Reimplemented for internal reasons.
Definition: addresseelineedit.cpp:1404
KLDAP::LdapResult::clientNumber
int clientNumber
The client the contact comes from (used for sorting in a ldap-only lookup).
Definition: ldapclientsearch.h:53
KPIM::AddresseeLineEdit::dropEvent
virtual void dropEvent(QDropEvent *)
Reimplemented for smart insertion of dragged email addresses.
Definition: addresseelineedit.cpp:1279
KPIM::AddresseeLineEdit::allowSemicolonAsSeparator
void allowSemicolonAsSeparator(bool allow)
Sets whether semicolons are allowed as separators.
Definition: addresseelineedit.cpp:1120
ldapclientsearch.h
KLDAP::LdapClient::completionWeight
int completionWeight() const
Returns the completion weight of this client.
Definition: ldapclient.cpp:312
newLineEditObjectName
static QString newLineEditObjectName()
Definition: addresseelineedit.cpp:194
KLDAP::LdapClientSearch
Definition: ldapclientsearch.h:60
KPIM::AddresseeLineEdit::textCompleted
void textCompleted()
QWidget
KLDAP::LdapResult
Describes the result returned by an LdapClientSearch query.
Definition: ldapclientsearch.h:44
QObject
KPIM::AddresseeLineEdit::addContact
void addContact(const KABC::Addressee &contact, int weight, int source=-1)
Adds a new contact to the completion with a given weight and source index.
Definition: addresseelineedit.cpp:1360
KLDAP::LdapResult::name
QString name
The full name of the contact.
Definition: ldapclientsearch.h:51
kmailcompletion.h
KPIM::AddresseeLineEdit::addItem
void addItem(const Akonadi::Item &item, int weight, int source=-1)
Definition: addresseelineedit.cpp:1346
KPIM::AddresseeLineEdit::~AddresseeLineEdit
virtual ~AddresseeLineEdit()
Destroys the addressee line edit.
Definition: addresseelineedit.cpp:1100
s_completionItemIndentString
static const QString s_completionItemIndentString
Definition: addresseelineedit.cpp:205
KPIM::AddresseeLineEdit::keyPressEvent
virtual void keyPressEvent(QKeyEvent *)
Reimplemented for internal reasons.
Definition: addresseelineedit.cpp:1125
addresseelineedit.h
KPIM::AddresseeLineEdit
Definition: addresseelineedit.h:61
KPIM::AddresseeLineEdit::removeCompletionSource
void removeCompletionSource(const QString &source)
Definition: addresseelineedit.cpp:1432
KPIM::AddresseeLineEdit::createStandardContextMenu
virtual QMenu * createStandardContextMenu()
Reimplemented for subclass access to menu.
Definition: addresseelineedit.cpp:1413
KPIM::AddresseeLineEdit::insert
virtual void insert(const QString &)
Reimplemented for smart insertion of email addresses.
Definition: addresseelineedit.cpp:1181
KPIM::AddresseeLineEdit::setText
virtual void setText(const QString &text)
Reimplemented for stripping whitespace after completion Danger: This is not virtual in the base class...
Definition: addresseelineedit.cpp:1245
KPIM::AddresseeLineEdit::cursorAtEnd
void cursorAtEnd()
Moves the cursor at the end of the line edit.
Definition: addresseelineedit.cpp:1336
KPIM::AddresseeLineEdit::addCompletionSource
int addCompletionSource(const QString &name, int weight)
Adds the name of a completion source and its weight to the internal list of completion sources and re...
Definition: addresseelineedit.cpp:1437
KPIM::AddresseeLineEdit::AddresseeLineEdit
AddresseeLineEdit(QWidget *parent, bool enableCompletion=true)
Creates a new addressee line edit.
Definition: addresseelineedit.cpp:1091
itemIsHeader
static bool itemIsHeader(const QListWidgetItem *item)
Definition: addresseelineedit.cpp:207
completionordereditor.h
KPIM::KMailCompletion
KMailCompletion allows lookup of email addresses by keyword.
Definition: kmailcompletion.h:39
QMenu
KPIM::AddresseeLineEdit::mouseReleaseEvent
virtual void mouseReleaseEvent(QMouseEvent *)
Reimplemented for smart insertion with middle mouse button.
Definition: addresseelineedit.cpp:1262
KLineEdit
KPIM::AddresseeLineEdit::paste
virtual void paste()
Reimplemented for smart insertion of pasted email addresses.
Definition: addresseelineedit.cpp:1252
KPIM::AddresseeLineEdit::setFont
void setFont(const QFont &font)
Reimplemented for setting the font for line edit and completion box.
Definition: addresseelineedit.cpp:1111
KPIM::CompletionItemsMap
QMap< QString, QPair< int, int > > CompletionItemsMap
Definition: addresseelineedit.cpp:83
KLDAP::LdapResult::email
QStringList email
The list of emails of the contact.
Definition: ldapclientsearch.h:52
KLDAP::LdapClient
An object that represents a configured LDAP server.
Definition: ldapclient.h:48
KPIM::AddresseeLineEdit::enableCompletion
void enableCompletion(bool enable)
Sets whether autocompletion shall be enabled.
Definition: addresseelineedit.cpp:1341
KJob
KLDAP::LdapResult::completionWeight
int completionWeight
The weight of the contact (used for sorting in a completion list).
Definition: ldapclientsearch.h:54
QList
KPIM::AddresseeLineEdit::addContactGroup
void addContactGroup(const KABC::ContactGroup &group, int weight, int source=-1)
Same as the above, but this time with contact groups.
Definition: addresseelineedit.cpp:1355
This file is part of the KDE documentation.
Documentation copyright © 1996-2014 The KDE developers.
Generated on Tue Oct 14 2014 22:58:03 by doxygen 1.8.7 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

libkdepim

Skip menu "libkdepim"
  • Main Page
  • Namespace List
  • Namespace Members
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • File Members
  • Modules

kdepim API Reference

Skip menu "kdepim API Reference"
  • akonadi_next
  • akregator
  • blogilo
  • calendarsupport
  • console
  •   kabcclient
  •   konsolekalendar
  • kaddressbook
  • kalarm
  •   lib
  • kdgantt2
  • kjots
  • kleopatra
  • kmail
  • knode
  • knotes
  • kontact
  • korgac
  • korganizer
  • ktimetracker
  • libkdepim
  • libkleo
  • libkpgp
  • mailcommon
  • messagelist
  • messageviewer

Search



Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal