1/*
2 This file is part of libkabc.
3 Copyright (c) 2002 Helge Deller <deller@gmx.de>
4 2002 Lubos Lunak <llunak@suse.cz>
5 2001,2003 Carsten Pfeiffer <pfeiffer@kde.org>
6 2001 Waldo Bastian <bastian@kde.org>
7
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Library General Public
10 License as published by the Free Software Foundation; either
11 version 2 of the License, or (at your option) any later version.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Library General Public License for more details.
17
18 You should have received a copy of the GNU Library General Public License
19 along with this library; see the file COPYING.LIB. If not, write to
20 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 Boston, MA 02110-1301, USA.
22*/
23
24#include "addresslineedit.h"
25
26#include <QApplication>
27#include <QKeyEvent>
28#include <QMouseEvent>
29#include <QtCore/QObject>
30#include <QtCore/QRegExp>
31
32#include <kcompletionbox.h>
33#include <kconfig.h>
34#include <kcursor.h>
35#include <kdebug.h>
36#include <kstandarddirs.h>
37#include <kstandardshortcut.h>
38
39#include "stdaddressbook.h"
40
41//=============================================================================
42//
43// Class AddressLineEdit
44//
45//=============================================================================
46
47using namespace KABC;
48
49class AddressLineEdit::Private
50{
51 public:
52 Private( AddressLineEdit *parent )
53 : mParent( parent ),
54 mCompletionInitialized( false ),
55 mSmartPaste( false )
56 {
57 init();
58 }
59
60 void init();
61 QStringList addresses();
62 QStringList removeMailDupes( const QStringList &adrs );
63
64 void slotCompletion() { mParent->doCompletion( false ); }
65 void slotPopupCompletion( const QString &completion );
66
67 AddressLineEdit *mParent;
68 QString mPreviousAddresses;
69 bool mUseCompletion;
70 bool mCompletionInitialized;
71 bool mSmartPaste;
72
73 static bool sAddressesDirty;
74 static bool initialized;
75};
76bool AddressLineEdit::Private::initialized = false;
77K_GLOBAL_STATIC( KCompletion, sCompletion )
78
79void AddressLineEdit::Private::init()
80{
81 if ( !Private::initialized ) {
82 Private::initialized = true;
83 sCompletion->setOrder( KCompletion::Sorted );
84 sCompletion->setIgnoreCase( true );
85 }
86
87 if ( mUseCompletion && !mCompletionInitialized ) {
88 mParent->setCompletionObject( sCompletion, false ); // we handle it ourself
89 mParent->connect( mParent, SIGNAL(completion(QString)),
90 mParent, SLOT(slotCompletion()) );
91
92 KCompletionBox *box = mParent->completionBox();
93 mParent->connect( box, SIGNAL(currentTextChanged(QString)),
94 mParent, SLOT(slotPopupCompletion(QString)) );
95 mParent->connect( box, SIGNAL(userCancelled(QString)),
96 SLOT(userCancelled(QString)) );
97
98 mCompletionInitialized = true; // don't connect muliple times. That's
99 // ugly, tho, better have completionBox()
100 // virtual in KDE 4
101 // Why? This is only called once. Why should this be called more
102 // than once? And why was this protected?
103 }
104}
105
106QStringList AddressLineEdit::Private::addresses()
107{
108 QApplication::setOverrideCursor( QCursor( Qt::WaitCursor ) ); // loading might take a while
109
110 QStringList result;
111 QLatin1String space( " " );
112 QRegExp needQuotes( QLatin1String( "[^ 0-9A-Za-z\\x0080-\\xFFFF]" ) );
113 QLatin1String endQuote( "\" " );
114 QString addr, email;
115
116 KABC::AddressBook *addressBook = KABC::StdAddressBook::self();
117 KABC::AddressBook::Iterator it;
118 for ( it = addressBook->begin(); it != addressBook->end(); ++it ) {
119 QStringList emails = ( *it ).emails();
120
121 QString n = ( *it ).prefix() + space +
122 ( *it ).givenName() + space +
123 ( *it ).additionalName() + space +
124 ( *it ).familyName() + space +
125 ( *it ).suffix();
126
127 n = n.simplified();
128
129 QStringList::ConstIterator mit;
130 QStringList::ConstIterator end( emails.constEnd() );
131
132 for ( mit = emails.constBegin(); mit != end; ++mit ) {
133 email = *mit;
134 if ( !email.isEmpty() ) {
135 if ( n.isEmpty() || ( email.indexOf( QLatin1Char( '<' ) ) != -1 ) ) {
136 addr.clear();
137 } else { /* do we really need quotes around this name ? */
138 if ( n.indexOf( needQuotes ) != -1 ) {
139 addr = QLatin1Char( '"' ) + n + endQuote;
140 } else {
141 addr = n + space;
142 }
143 }
144
145 if ( !addr.isEmpty() &&
146 ( email.indexOf( QLatin1Char( '<' ) ) == -1 ) &&
147 ( email.indexOf( QLatin1Char( '>' ) ) == -1 ) &&
148 ( email.indexOf( QLatin1Char( ',' ) ) == -1 ) ) {
149 addr += QLatin1Char( '<' ) + email + QLatin1Char( '>' );
150 } else {
151 addr += email;
152 }
153 addr = addr.trimmed();
154 result.append( addr );
155 }
156 }
157 }
158
159 result += addressBook->allDistributionListNames();
160
161 QApplication::restoreOverrideCursor();
162
163 return result;
164}
165
166QStringList AddressLineEdit::Private::removeMailDupes( const QStringList &addrs )
167{
168 QStringList src( addrs );
169 qSort( src );
170
171 QString last;
172 for ( QStringList::Iterator it = src.begin(); it != src.end(); ) {
173 if ( *it == last ) {
174 it = src.erase( it );
175 continue; // dupe
176 }
177
178 last = *it;
179 ++it;
180 }
181
182 return src;
183}
184
185void AddressLineEdit::Private::slotPopupCompletion( const QString &completion )
186{
187 mParent->setText( mPreviousAddresses + completion );
188 mParent->cursorAtEnd();
189}
190
191bool AddressLineEdit::Private::sAddressesDirty = false;
192
193AddressLineEdit::AddressLineEdit( QWidget *parent, bool useCompletion )
194 : KLineEdit( parent ), d( new Private( this ) )
195{
196 d->mUseCompletion = useCompletion;
197
198 // Whenever a new AddressLineEdit is created (== a new composer is created),
199 // we set a dirty flag to reload the addresses upon the first completion.
200 // The address completions are shared between all AddressLineEdits.
201 // Is there a signal that tells us about addressbook updates?
202 if ( d->mUseCompletion ) {
203 d->sAddressesDirty = true;
204 }
205}
206
207//-----------------------------------------------------------------------------
208AddressLineEdit::~AddressLineEdit()
209{
210 delete d;
211}
212
213//-----------------------------------------------------------------------------
214
215void AddressLineEdit::setFont( const QFont &font )
216{
217 KLineEdit::setFont( font );
218 if ( d->mUseCompletion ) {
219 completionBox()->setFont( font );
220 }
221}
222
223//-----------------------------------------------------------------------------
224void AddressLineEdit::keyPressEvent( QKeyEvent *event )
225{
226 bool accept = false;
227
228 if ( KStandardShortcut::shortcut( KStandardShortcut::SubstringCompletion ).
229 contains( event->key() | event->modifiers() ) ) {
230 doCompletion( true );
231 accept = true;
232 } else if ( KStandardShortcut::shortcut( KStandardShortcut::TextCompletion ).
233 contains( event->key() | event->modifiers() ) ) {
234 int len = text().length();
235
236 if ( len == cursorPosition() ) { // at End?
237 doCompletion( true );
238 accept = true;
239 }
240 }
241
242 if ( !accept ) {
243 KLineEdit::keyPressEvent( event );
244 }
245}
246
247void AddressLineEdit::mouseReleaseEvent( QMouseEvent *event )
248{
249 if ( d->mUseCompletion && ( event->button() == Qt::MidButton ) ) {
250 d->mSmartPaste = true;
251 KLineEdit::mouseReleaseEvent( event );
252 d->mSmartPaste = false;
253 return;
254 }
255
256 KLineEdit::mouseReleaseEvent( event );
257}
258
259void AddressLineEdit::insert( const QString &oldText )
260{
261 if ( !d->mSmartPaste ) {
262 KLineEdit::insert( oldText );
263 return;
264 }
265
266 QString newText = oldText.trimmed();
267 if ( newText.isEmpty() ) {
268 return;
269 }
270
271 // remove newlines in the to-be-pasted string as well as an eventual
272 // mailto: protocol
273 newText.replace( QRegExp( QLatin1String( "\r?\n" ) ), QLatin1String( ", " ) );
274 if ( newText.startsWith( QLatin1String( "mailto:" ) ) ) {
275 KUrl u( newText );
276 newText = u.path();
277 } else if ( newText.indexOf( QLatin1String( " at " ) ) != -1 ) {
278 // Anti-spam stuff
279 newText.replace( QLatin1String( " at " ), QLatin1String( "@" ) );
280 newText.replace( QLatin1String( " dot " ), QLatin1String( "." ) );
281 } else if ( newText.indexOf( QLatin1String( "(at)" ) ) != -1 ) {
282 newText.replace( QRegExp( QLatin1String( "\\s*\\(at\\)\\s*" ) ), QLatin1String( "@" ) );
283 }
284
285 QString contents = text();
286 int start_sel = 0;
287 int end_sel = 0;
288 int pos = cursorPosition();
289 if ( !selectedText().isEmpty() ) {
290 // Cut away the selection.
291 if ( pos > end_sel ) {
292 pos -= ( end_sel - start_sel );
293 } else if ( pos > start_sel ) {
294 pos = start_sel;
295 }
296 contents = contents.left( start_sel ) + contents.right( end_sel + 1 );
297 }
298
299 int eot = contents.length();
300 while ( ( eot > 0 ) && contents[ eot - 1 ].isSpace() ) {
301 eot--;
302 }
303
304 if ( eot == 0 ) {
305 contents.clear();
306 } else if ( pos >= eot ) {
307 if ( contents[ eot - 1 ] == QLatin1Char( ',' ) ) {
308 eot--;
309 }
310 contents.truncate( eot );
311 contents += QLatin1String( ", " );
312 pos = eot+2;
313 }
314
315 contents = contents.left( pos ) + newText + contents.mid( pos );
316 setText( contents );
317 setCursorPosition( pos + newText.length() );
318}
319
320void AddressLineEdit::paste()
321{
322 if ( d->mUseCompletion ) {
323 d->mSmartPaste = true;
324 }
325
326 KLineEdit::paste();
327 d->mSmartPaste = false;
328}
329
330//-----------------------------------------------------------------------------
331void AddressLineEdit::cursorAtEnd()
332{
333 setCursorPosition( text().length() );
334}
335
336//-----------------------------------------------------------------------------
337void AddressLineEdit::enableCompletion( bool enable )
338{
339 d->mUseCompletion = enable;
340}
341
342//-----------------------------------------------------------------------------
343void AddressLineEdit::doCompletion( bool ctrlT )
344{
345 if ( !d->mUseCompletion ) {
346 return;
347 }
348
349 QString prevAddr;
350
351 QString s( text() );
352 int n = s.lastIndexOf( QLatin1Char( ',' ) );
353
354 if ( n >= 0 ) {
355 n++; // Go past the ","
356
357 int len = s.length();
358
359 // Increment past any whitespace...
360 while ( n < len && s[ n ].isSpace() ) {
361 n++;
362 }
363
364 prevAddr = s.left( n );
365 s = s.mid( n, 255 ).trimmed();
366 }
367
368 if ( d->sAddressesDirty ) {
369 loadAddresses();
370 }
371
372 if ( ctrlT ) {
373 QStringList completions = sCompletion->substringCompletion( s );
374 if ( completions.count() > 1 ) {
375 d->mPreviousAddresses = prevAddr;
376 setCompletedItems( completions );
377 } else if ( completions.count() == 1 ) {
378 setText( prevAddr + completions.first() );
379 }
380
381 cursorAtEnd();
382 return;
383 }
384
385 KGlobalSettings::Completion mode = completionMode();
386
387 switch ( mode ) {
388 case KGlobalSettings::CompletionPopupAuto:
389 {
390 if ( s.isEmpty() ) {
391 break;
392 }
393 }
394 case KGlobalSettings::CompletionPopup:
395 {
396 d->mPreviousAddresses = prevAddr;
397 QStringList items = sCompletion->allMatches( s );
398 items += sCompletion->allMatches( QLatin1String( "\"" ) + s );
399 items += sCompletion->substringCompletion( QLatin1Char( '<' ) + s );
400 int beforeDollarCompletionCount = items.count();
401
402 if ( s.indexOf( QLatin1Char( ' ' ) ) == -1 ) { // one word, possibly given name
403 items += sCompletion->allMatches( QLatin1String( "$$" ) + s );
404 }
405
406 if ( !items.isEmpty() ) {
407 if ( items.count() > beforeDollarCompletionCount ) {
408 // remove the '$$whatever$' part
409 for ( QStringList::Iterator it = items.begin();
410 it != items.end(); ++it ) {
411 int pos = ( *it ).indexOf( QLatin1Char( '$' ), 2 );
412 if ( pos < 0 ) { // ???
413 continue;
414 }
415 ( *it ) = ( *it ).mid( pos + 1 );
416 }
417 }
418
419 items = d->removeMailDupes( items );
420
421 // We do not want KLineEdit::setCompletedItems to perform text
422 // completion (suggestion) since it does not know how to deal
423 // with providing proper completions for different items on the
424 // same line, e.g. comma-separated list of email addresses.
425 bool autoSuggest = ( mode != KGlobalSettings::CompletionPopupAuto );
426 setCompletedItems( items, autoSuggest );
427
428 if ( !autoSuggest ) {
429 int index = items.first().indexOf( s );
430 QString newText = prevAddr + items.first().mid( index );
431 //kDebug() << "OLD TEXT:" << text();
432 //kDebug() << "NEW TEXT:" << newText;
433 setUserSelection( false );
434 setCompletedText( newText, true );
435 }
436 }
437
438 break;
439 }
440
441 case KGlobalSettings::CompletionShell:
442 {
443 QString match = sCompletion->makeCompletion( s );
444 if ( !match.isNull() && match != s ) {
445 setText( prevAddr + match );
446 cursorAtEnd();
447 }
448 break;
449 }
450
451 case KGlobalSettings::CompletionMan: // Short-Auto in fact
452 case KGlobalSettings::CompletionAuto:
453 {
454 if ( !s.isEmpty() ) {
455 QString match = sCompletion->makeCompletion( s );
456 if ( !match.isNull() && match != s ) {
457 setCompletedText( prevAddr + match );
458 }
459
460 break;
461 }
462 }
463 case KGlobalSettings::CompletionNone:
464 default: // fall through
465 break;
466 }
467}
468
469//-----------------------------------------------------------------------------
470void AddressLineEdit::loadAddresses()
471{
472 sCompletion->clear();
473 d->sAddressesDirty = false;
474
475 const QStringList addrs = d->addresses();
476 QStringList::ConstIterator end( addrs.end() );
477 for ( QStringList::ConstIterator it = addrs.begin(); it != end; ++it ) {
478 addAddress( *it );
479 }
480}
481
482void AddressLineEdit::addAddress( const QString &addr )
483{
484 sCompletion->addItem( addr );
485
486 int pos = addr.indexOf( QLatin1Char( '<' ) );
487 if ( pos >= 0 ) {
488 ++pos;
489 int pos2 = addr.indexOf( QLatin1Char( '>' ), pos );
490 if ( pos2 >= 0 ) {
491 sCompletion->addItem( addr.mid( pos, pos2 - pos ) );
492 }
493 }
494}
495
496//-----------------------------------------------------------------------------
497void AddressLineEdit::dropEvent( QDropEvent *event )
498{
499 const KUrl::List uriList = KUrl::List::fromMimeData( event->mimeData() );
500 if ( !uriList.isEmpty() ) {
501 QString ct = text();
502 KUrl::List::ConstIterator it = uriList.begin();
503 for ( ; it != uriList.end(); ++it ) {
504 if ( !ct.isEmpty() ) {
505 ct.append( QLatin1String( ", " ) );
506 }
507
508 KUrl u( *it );
509 if ( ( *it ).protocol() == QLatin1String( "mailto" ) ) {
510 ct.append( ( *it ).path() );
511 } else {
512 ct.append( ( *it ).url() );
513 }
514 }
515 setText( ct );
516 setModified( true );
517 } else {
518 if ( d->mUseCompletion ) {
519 d->mSmartPaste = true;
520 }
521
522 KLineEdit::dropEvent( event );
523 d->mSmartPaste = false;
524 }
525}
526
527#include "moc_addresslineedit.cpp"
528