Pimcommon

addresseelineedit.cpp
1/*
2 This file is part of libkdepim.
3
4 SPDX-FileCopyrightText: 2002 Helge Deller <deller@gmx.de>
5 SPDX-FileCopyrightText: 2002 Lubos Lunak <llunak@suse.cz>
6 SPDX-FileCopyrightText: 2001, 2003 Carsten Pfeiffer <pfeiffer@kde.org>
7 SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
8 SPDX-FileCopyrightText: 2004 Daniel Molkentin <danimo@klaralvdalens-datakonsult.se>
9 SPDX-FileCopyrightText: 2004 Karl-Heinz Zimmer <khz@klaralvdalens-datakonsult.se>
10 SPDX-FileCopyrightText: 2017-2024 Laurent Montel <montel@kde.org>
11
12 SPDX-License-Identifier: LGPL-2.0-or-later
13*/
14
15#include "addresseelineedit.h"
16using namespace Qt::Literals::StringLiterals;
17
18#include "addresseelineedit_p.h"
19#include "addresseelineeditmanager.h"
20#include "addresseelineeditutil.h"
21#include "addressline/recentaddress/recentaddresses.h"
22#include <KLDAPCore/LdapClientSearch>
23
24#include <KContacts/VCardConverter>
25
26#include <Akonadi/Job>
27#include <KConfigGroup>
28#include <QUrl>
29
30#include <Akonadi/ContactGroupExpandJob>
31#include <Akonadi/ContactGroupSearchJob>
32#include <KColorScheme>
33#include <KContacts/ContactGroupTool>
34#include <KEmailAddress>
35#include <KIO/StoredTransferJob>
36#include <KJobWidgets>
37
38#include "pimcommonakonadi_debug.h"
39#include <KCodecs>
40#include <KCompletionBox>
41#include <KLocalizedString>
42#include <KStandardShortcut>
43
44#include "addressline/completionconfiguredialog/completionconfiguredialog.h"
45#include <Akonadi/ContactGroupExpandJob>
46#include <KContacts/VCardDrag>
47#include <KMessageBox>
48#include <KSharedConfig>
49#include <QApplication>
50#include <QBuffer>
51#include <QClipboard>
52#include <QDropEvent>
53#include <QEvent>
54#include <QKeyEvent>
55#include <QMenu>
56#include <QMimeData>
57#include <QMouseEvent>
58#include <QObject>
59
60using namespace PimCommon;
61
62inline bool itemIsHeader(const QListWidgetItem *item)
63{
64 return item && !item->text().startsWith(" "_L1);
65}
66
67// needs to be unique, but the actual name doesn't matter much
68static QString newLineEditObjectName()
69{
70 static int s_count = 0;
71 QString name(QStringLiteral("KPIM::AddresseeLineEdit"));
72 if (s_count++) {
73 name += QLatin1Char('-');
74 name += QString::number(s_count);
75 }
76 return name;
77}
78
79AddresseeLineEdit::AddresseeLineEdit(QWidget *parent, bool enableCompletion)
80 : KLineEdit(parent)
81 , d(new AddresseeLineEditPrivate(this, enableCompletion))
82{
83 setObjectName(newLineEditObjectName());
84 setPlaceholderText(QString());
85
86 d->init();
87}
88
89AddresseeLineEdit::~AddresseeLineEdit()
90{
91 delete d;
92}
93
94void AddresseeLineEdit::setFont(const QFont &font)
95{
97
98 if (d->useCompletion()) {
100 }
101}
102
103void AddresseeLineEdit::setIcon(const QIcon &icon, const QString &tooltip)
104{
105 d->setIcon(icon, tooltip);
106}
107
108bool AddresseeLineEdit::expandIntern() const
109{
110 return d->expandIntern();
111}
112
113void AddresseeLineEdit::setExpandIntern(bool expand)
114{
115 d->setExpandIntern(expand);
116}
117
118void AddresseeLineEdit::setEnableBalooSearch(bool enable)
119{
120 d->setEnableBalooSearch(enable);
121}
122
123bool AddresseeLineEdit::enableBalooSearch() const
124{
125 return d->enableAkonadiSearch();
126}
127
128void AddresseeLineEdit::setEnableAkonadiSearch(bool enable)
129{
130 d->setEnableAkonadiSearch(enable);
131}
132
133bool AddresseeLineEdit::enableAkonadiSearch() const
134{
135 return d->enableAkonadiSearch();
136}
137
138void AddresseeLineEdit::allowSemicolonAsSeparator(bool useSemicolonAsSeparator)
139{
140 d->setUseSemicolonAsSeparator(useSemicolonAsSeparator);
141}
142
143bool AddresseeLineEdit::showRecentAddresses() const
144{
145 return d->showRecentAddresses();
146}
147
148void AddresseeLineEdit::setShowRecentAddresses(bool b)
149{
150 d->setShowRecentAddresses(b);
151}
152
153void AddresseeLineEdit::keyPressEvent(QKeyEvent *event)
154{
155 bool accept = false;
156
157 const int key = event->key() | event->modifiers();
158
160 // TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch
161 d->updateSearchString();
162 d->startSearches();
163 d->doCompletion(true);
164 accept = true;
166 const int len = text().length();
167
168 if (len == cursorPosition()) { // at End?
169 d->updateSearchString();
170 d->startSearches();
171 d->doCompletion(true);
172 accept = true;
173 }
174 }
175
176 const QString oldContent = text();
177 if (!accept) {
179 }
180
181 // if the text didn't change (eg. because a cursor navigation key was pressed)
182 // we don't need to trigger a new search
183 if (oldContent == text()) {
184 return;
185 }
186
187 if (event->isAccepted()) {
188 d->updateSearchString();
189
190 QString searchString(d->searchString());
191 // LDAP does not know about our string manipulation, remove it
192 if (d->searchExtended()) {
193 searchString = d->searchString().mid(1);
194 }
195
196 d->restartTime(searchString);
197 }
198}
199
200void AddresseeLineEdit::insert(const QString &t)
201{
202 if (!d->smartPaste()) {
204 return;
205 }
206
207 QString newText = t.trimmed();
208 if (newText.isEmpty()) {
209 return;
210 }
211
212 newText = PimCommon::AddresseeLineEditUtil::adaptPasteMails(newText);
213
214 QString contents = text();
215 int pos = cursorPosition();
216
217 if (hasSelectedText()) {
218 // Cut away the selection.
219 int start_sel = selectionStart();
220 pos = start_sel;
221 contents = contents.left(start_sel) + contents.mid(start_sel + selectedText().length());
222 }
223
224 int eot = contents.length();
225 while ((eot > 0) && contents.at(eot - 1).isSpace()) {
226 --eot;
227 }
228 if (eot == 0) {
229 contents.clear();
230 } else if (pos >= eot) {
231 if (contents.at(eot - 1) == QLatin1Char(',')) {
232 --eot;
233 }
234 contents.truncate(eot);
235 contents += QStringLiteral(", ");
236 pos = eot + 2;
237 }
238
239 contents = contents.left(pos) + newText + contents.mid(pos);
240 setText(contents);
241 setModified(true);
242 setCursorPosition(pos + newText.length());
243}
244
245void AddresseeLineEdit::setText(const QString &text)
246{
247 const int cursorPos = cursorPosition();
248 KLineEdit::setText(text.trimmed());
249 setCursorPosition(cursorPos);
250}
251
252void AddresseeLineEdit::paste()
253{
254 if (d->useCompletion()) {
255 d->setSmartPaste(true);
256 }
257
259 d->setSmartPaste(false);
260}
261
262void AddresseeLineEdit::mouseReleaseEvent(QMouseEvent *event)
263{
264 // reimplemented from QLineEdit::mouseReleaseEvent()
265 if (d->useCompletion() && QApplication::clipboard()->supportsSelection() && !isReadOnly() && event->button() == Qt::MiddleButton) {
266 d->setSmartPaste(true);
267 }
268
270 d->setSmartPaste(false);
271}
272
273void AddresseeLineEdit::dropEvent(QDropEvent *event)
274{
275 const QMimeData *md = event->mimeData();
276 // Case one: The user dropped a text/directory (i.e. vcard), so decode its
277 // contents
281
282 for (const KContacts::Addressee &addr : std::as_const(list)) {
283 insertEmails(addr.emails());
284 }
285 }
286 // Case two: The user dropped a list or Urls.
287 // Iterate over that list. For mailto: Urls, just add the addressee to the list,
288 // and for other Urls, download the Url and assume it points to a vCard
289 else if (md->hasUrls()) {
290 const QList<QUrl> urls = md->urls();
292
293 for (const QUrl &url : urls) {
294 // First, let's deal with mailto Urls. The path() part contains the
295 // email-address.
296 if (url.scheme() == "mailto"_L1) {
297 KContacts::Addressee addressee;
299 email.setPreferred(true);
300 addressee.addEmail(email);
301 list += addressee;
302 } else { // Otherwise, download the vCard to which the Url points
304 auto job = KIO::storedGet(url);
306 if (job->exec()) {
307 QByteArray data = job->data();
308 list += converter.parseVCards(data);
309
310 if (list.isEmpty()) { // try to parse a contact group
312 QBuffer dataStream(&data);
313 dataStream.open(QIODevice::ReadOnly);
315 if (KContacts::ContactGroupTool::convertFromXml(&dataStream, group, &error)) {
316 auto expandJob = new Akonadi::ContactGroupExpandJob(group);
317 connect(expandJob, &Akonadi::ContactGroupExpandJob::result, this, &AddresseeLineEdit::groupExpandResult);
318 expandJob->start();
319 } else {
320 qCWarning(PIMCOMMONAKONADI_LOG) << "Error during converting contactgroup " << error;
321 }
322 }
323 } else {
324 const QString caption(i18n("vCard Import Failed"));
325 const QString text = i18n("<qt>Unable to access <b>%1</b>.</qt>", url.url());
327 }
328 }
329 }
330
331 // Now, let the user choose which addressee to add.
332 for (const KContacts::Addressee &addressee : std::as_const(list)) {
333 insertEmails(addressee.emails());
334 }
335 }
336 // Case three: Let AddresseeLineEdit deal with the rest
337 else {
338 if (!isReadOnly()) {
339 const QList<QUrl> uriList = event->mimeData()->urls();
340 if (!uriList.isEmpty()) {
341 QString contents = text();
342 // remove trailing white space and comma
343 int eot = contents.length();
344 while ((eot > 0) && contents.at(eot - 1).isSpace()) {
345 --eot;
346 }
347 if (eot == 0) {
348 contents.clear();
349 } else if (contents.at(eot - 1) == QLatin1Char(',')) {
350 --eot;
351 contents.truncate(eot);
352 }
353 bool mailtoURL = false;
354 // append the mailto URLs
355 for (const QUrl &url : uriList) {
356 if (url.scheme() == "mailto"_L1) {
357 mailtoURL = true;
359 address = QUrl::fromPercentEncoding(url.path().toLatin1());
361 if (!contents.isEmpty()) {
362 contents.append(", "_L1);
363 }
364 contents.append(address);
365 }
366 }
367 if (mailtoURL) {
368 setText(contents);
369 setModified(true);
370 return;
371 }
372 } else {
373 // Let's see if this drop contains a comma separated list of emails
374 if (md->hasText()) {
375 const QString dropData = md->text();
376 const QStringList addrs = KEmailAddress::splitAddressList(dropData);
377 if (!addrs.isEmpty()) {
378 if (addrs.count() == 1) {
379 QUrl url(dropData);
380 if (url.scheme() == "mailto"_L1) {
381 KContacts::Addressee addressee;
383 email.setPreferred(true);
384 addressee.addEmail(email);
385 insertEmails(addressee.emails());
386 } else {
388 }
389 } else {
391 }
392 setModified(true);
393 return;
394 }
395 }
396 }
397 }
398
399 if (d->useCompletion()) {
400 d->setSmartPaste(true);
401 }
402
404 d->setSmartPaste(false);
405 }
406}
407
408void AddresseeLineEdit::groupExpandResult(KJob *job)
409{
411
412 if (!expandJob) {
413 return;
414 }
415
416 const KContacts::Addressee::List contacts = expandJob->contacts();
417 for (const KContacts::Addressee &addressee : contacts) {
418 if (d->expandIntern() || text().trimmed().isEmpty()) {
419 insertEmails({addressee.fullEmail()});
420 } else {
421 Q_EMIT addAddress(addressee.fullEmail());
422 }
423 }
424
425 job->deleteLater();
426}
427
428void AddresseeLineEdit::insertEmails(const QStringList &emails)
429{
430 if (emails.empty()) {
431 return;
432 }
433
434 QString contents = text();
435 if (!contents.isEmpty()) {
436 contents += QLatin1Char(',');
437 }
438 // only one address, don't need kpopup to choose
439 if (emails.size() == 1) {
440 setText(contents + emails.front());
441 return;
442 }
443 // multiple emails, let the user choose one
444 QMenu menu(this);
445 menu.setTitle(i18n("Select email from contact"));
446 menu.setObjectName("Addresschooser"_L1);
447 for (const QString &email : emails) {
448 menu.addAction(email);
449 }
450 const QAction *result = menu.exec(QCursor::pos());
451 if (!result) {
452 return;
453 }
454 setText(contents + KLocalizedString::removeAcceleratorMarker(result->text()));
455}
456
457void AddresseeLineEdit::cursorAtEnd()
458{
459 setCursorPosition(text().length());
460}
461
462void AddresseeLineEdit::enableCompletion(bool enable)
463{
464 d->setUseCompletion(enable);
465}
466
467bool AddresseeLineEdit::isCompletionEnabled() const
468{
469 return d->useCompletion();
470}
471
472void AddresseeLineEdit::addItem(const Akonadi::Item &item, int weight, int source)
473{
474 // Let Akonadi results always have a higher weight than baloo results
475 if (item.hasPayload<KContacts::Addressee>()) {
476 addContact(item.payload<KContacts::Addressee>(), weight + 1, source);
477 } else if (item.hasPayload<KContacts::ContactGroup>()) {
478 addContactGroup(item.payload<KContacts::ContactGroup>(), weight + 1, source);
479 }
480}
481
482void AddresseeLineEdit::addContactGroup(const KContacts::ContactGroup &group, int weight, int source)
483{
484 d->addCompletionItem(group.name(), weight, source);
485}
486
487void AddresseeLineEdit::addContact(const QStringList &emails, const KContacts::Addressee &addr, int weight, int source, QString append)
488{
489 int isPrefEmail = 1; // first in list is preferredEmail
490 for (const QString &email : emails) {
491 // TODO: highlight preferredEmail
492 const QString givenName = addr.givenName();
493 const QString familyName = addr.familyName();
494 const QString nickName = addr.nickName();
495 const QString fullEmail = addr.fullEmail(email);
496
497 QString appendix;
498
499 if (!append.isEmpty()) {
500 appendix = QStringLiteral(" (%1)");
501 append.replace(QLatin1Char('('), QStringLiteral("["));
502 append.replace(QLatin1Char(')'), QStringLiteral("]"));
503 appendix = appendix.arg(append);
504 }
505
506 // Prepare "givenName" + ' ' + "familyName"
507 QString fullName = givenName;
508 if (!familyName.isEmpty()) {
509 if (!fullName.isEmpty()) {
510 fullName += QLatin1Char(' ');
511 }
512 fullName += familyName;
513 }
514
515 // Finally, we can add the completion items
516 if (!fullName.isEmpty()) {
517 const QString address = KEmailAddress::normalizedAddress(fullName, email, QString());
518 if (fullEmail != address) {
519 // This happens when fullEmail contains a middle name, while our own fullName+email only has "first last".
520 // Let's offer both, the fullEmail with 3 parts, looks a tad formal.
521 d->addCompletionItem(address + appendix, weight + isPrefEmail, source);
522 }
523 }
524
525 QStringList keyWords;
526 if (!nickName.isEmpty()) {
527 keyWords.append(nickName);
528 }
529
530 d->addCompletionItem(fullEmail + appendix, weight + isPrefEmail, source, &keyWords);
531
532 isPrefEmail = 0;
533 }
534}
535
536void AddresseeLineEdit::addContact(const KContacts::Addressee &addr, int weight, int source, const QString &append)
537{
538 const QStringList emails = AddresseeLineEditManager::self()->cleanupEmailList(addr.emails());
539 if (emails.isEmpty()) {
540 return;
541 }
542 addContact(emails, addr, weight, source, append);
543}
544
545void AddresseeLineEdit::contextMenuEvent(QContextMenuEvent *event)
546{
547 QMenu *menu = createStandardContextMenu();
548 if (menu) { // can be 0 on platforms with only a touch interface
549 menu->exec(event->globalPos());
550 delete menu;
551 }
552}
553
554QMenu *AddresseeLineEdit::createStandardContextMenu()
555{
556 // disable modes not supported by KMailCompletion
559
561 if (!menu) {
562 return nullptr;
563 }
564 if (d->useCompletion()) {
565 auto showOU = new QAction(i18nc("@action", "Show Organization Unit for LDAP results"), menu);
566 showOU->setCheckable(true);
567 showOU->setChecked(d->showOU());
568 connect(showOU, &QAction::triggered, d, &AddresseeLineEditPrivate::slotShowOUChanged);
569 menu->addAction(showOU);
570 }
571 if (isCompletionEnabled()) {
572 menu->addSeparator();
573 QAction *act = menu->addAction(i18n("Configure Completion..."));
574 connect(act, &QAction::triggered, this, &AddresseeLineEdit::configureCompletion);
575 }
576 menu->addSeparator();
577 QAction *act = menu->addAction(i18n("Automatically expand groups"));
578 act->setCheckable(true);
579 act->setChecked(d->autoGroupExpand());
580 connect(act, &QAction::triggered, d, &AddresseeLineEditPrivate::slotToggleExpandGroups);
581
582 if (!d->groupsIsEmpty()) {
583 act = menu->addAction(i18n("Expand Groups..."));
584 connect(act, &QAction::triggered, this, &AddresseeLineEdit::expandGroups);
585 }
586 return menu;
587}
588
589bool AddresseeLineEdit::canDeleteLineEdit() const
590{
591 return d->canDeleteLineEdit();
592}
593
594void AddresseeLineEdit::configureCompletion()
595{
596 d->setCanDeleteLineEdit(false);
598 dlg->setRecentAddresses(PimCommon::RecentAddresses::self(recentAddressConfig())->addresses());
599 dlg->setLdapClientSearch(ldapSearch());
600 dlg->setEmailBlackList(PimCommon::AddresseeLineEditManager::self()->balooBlackList());
601 dlg->load();
602 if (dlg->exec() && dlg) {
603 if (dlg->recentAddressWasChanged()) {
604 PimCommon::RecentAddresses::self(recentAddressConfig())->clear();
605 dlg->storeAddresses(recentAddressConfig());
606 loadContacts();
607 }
608 updateBalooBlackList();
609 updateCompletionOrder();
610 }
611 d->setCanDeleteLineEdit(true);
612}
613
614void AddresseeLineEdit::loadContacts()
615{
616 const QString recentAddressGroupName = i18n("Recent Addresses");
617 if (showRecentAddresses()) {
618 const QStringList recent =
619 AddresseeLineEditManager::self()->cleanupRecentAddressEmailList(PimCommon::RecentAddresses::self(recentAddressConfig())->addresses());
621 QString emailString;
622
623 KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kpimcompletionorder"));
624 KConfigGroup group(config, QStringLiteral("CompletionWeights"));
625 const int weight = group.readEntry("Recent Addresses", 10);
626 removeCompletionSource(recentAddressGroupName);
627 const int idx = addCompletionSource(recentAddressGroupName, weight);
628
629 for (const QString &recentAdr : recent) {
631 KEmailAddress::extractEmailAddressAndName(recentAdr, emailString, name);
632 if (emailString.isEmpty()) {
633 continue;
634 }
636 if (!name.isEmpty() && (name[0] == QLatin1Char('"')) && (name[name.length() - 1] == QLatin1Char('"'))) {
637 name.remove(0, 1);
638 name.chop(1);
639 }
640 addr.setNameFromString(name);
641 KContacts::Email email(emailString);
642 email.setPreferred(true);
643 addr.addEmail(email);
644 addContact({emailString}, addr, weight, idx);
645 }
646 } else {
647 removeCompletionSource(recentAddressGroupName);
648 }
649}
650
651void AddresseeLineEdit::removeCompletionSource(const QString &source)
652{
653 d->removeCompletionSource(source);
654}
655
656int AddresseeLineEdit::addCompletionSource(const QString &source, int weight)
657{
658 return d->addCompletionSource(source, weight);
659}
660
661bool AddresseeLineEdit::eventFilter(QObject *object, QEvent *event)
662{
663 if (d->completionInitialized() && (object == completionBox() || completionBox()->findChild<QWidget *>(object->objectName()) == object)) {
665 || event->type() == QEvent::MouseButtonDblClick) {
666 const QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
667 // find list box item at the event position
668 QListWidgetItem *item = completionBox()->itemAt(mouseEvent->pos());
669 if (!item) {
670 // In the case of a mouse move outside of the box we don't want
671 // the parent to fuzzy select a header by mistake.
672 const bool eat = event->type() == QEvent::MouseMove;
673 return eat;
674 }
675 // avoid selection of headers on button press, or move or release while
676 // a button is pressed
677 const Qt::MouseButtons buttons = mouseEvent->buttons();
678 if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick || buttons & Qt::LeftButton
679 || buttons & Qt::MiddleButton || buttons & Qt::RightButton) {
680 if (itemIsHeader(item)) {
681 return true; // eat the event, we don't want anything to happen
682 } else {
683 // if we are not on one of the group heading, make sure the item
684 // below or above is selected, not the heading, inadvertedly, due
685 // to fuzzy auto-selection from QListBox
687 item->setSelected(true);
688 if (event->type() == QEvent::MouseMove) {
689 return true; // avoid fuzzy selection behavior
690 }
691 }
692 }
693 }
694 }
695
696 if ((object == this) && (event->type() == QEvent::ShortcutOverride)) {
697 auto keyEvent = static_cast<QKeyEvent *>(event);
698 if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Tab) {
699 keyEvent->accept();
700 return true;
701 }
702 }
703
704 if ((object == this) && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) && completionBox()->isVisible()) {
705 const QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
706 int currentIndex = completionBox()->currentRow();
707 if (currentIndex < 0) {
708 return true;
709 }
710 if (keyEvent->key() == Qt::Key_Up) {
711 // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex;
712 // figure out if the item we would be moving to is one we want
713 // to ignore. If so, go one further
714 const QListWidgetItem *itemAbove = completionBox()->item(currentIndex);
715 if (itemAbove && itemIsHeader(itemAbove)) {
716 // there is a header above is, check if there is even further up
717 // and if so go one up, so it'll be selected
718 if (currentIndex > 0 && completionBox()->item(currentIndex - 1)) {
719 // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1;
720 completionBox()->setCurrentRow(currentIndex - 1);
721 completionBox()->item(currentIndex - 1)->setSelected(true);
722 } else if (currentIndex == 0) {
723 // nothing to skip to, let's stay where we are, but make sure the
724 // first header becomes visible, if we are the first real entry
726 QListWidgetItem *item = completionBox()->item(currentIndex);
727 if (item) {
728 if (itemIsHeader(item)) {
729 currentIndex++;
730 item = completionBox()->item(currentIndex);
731 }
733 item->setSelected(true);
734 }
735 }
736
737 return true;
738 }
739 } else if (keyEvent->key() == Qt::Key_Down) {
740 // same strategy for downwards
741 // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex;
742 const QListWidgetItem *itemBelow = completionBox()->item(currentIndex);
743 if (itemBelow && itemIsHeader(itemBelow)) {
744 if (completionBox()->item(currentIndex + 1)) {
745 // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1;
746 completionBox()->setCurrentRow(currentIndex + 1);
747 completionBox()->item(currentIndex + 1)->setSelected(true);
748 } else {
749 // nothing to skip to, let's stay where we are
750 QListWidgetItem *item = completionBox()->item(currentIndex);
751 if (item) {
753 item->setSelected(true);
754 }
755 }
756
757 return true;
758 }
759 // special case of the initial selection, which is unfortunately a header.
760 // Setting it to selected tricks KCompletionBox into not treating is special
761 // and selecting making it current, instead of the one below.
762 QListWidgetItem *item = completionBox()->item(currentIndex);
763 if (item && itemIsHeader(item)) {
765 item->setSelected(true);
766 }
767 } else if (event->type() == QEvent::KeyRelease && (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab)) {
768 /// first, find the header of the current section
769 QListWidgetItem *myHeader = nullptr;
770 int myHeaderIndex = -1;
771 const int iterationStep = keyEvent->key() == Qt::Key_Tab ? 1 : -1;
772 int index = qMin(qMax(currentIndex - iterationStep, 0), completionBox()->count() - 1);
773 while (index >= 0) {
774 if (itemIsHeader(completionBox()->item(index))) {
775 myHeader = completionBox()->item(index);
776 myHeaderIndex = index;
777 break;
778 }
779
780 index--;
781 }
782 Q_ASSERT(myHeader); // we should always be able to find a header
783
784 // find the next header (searching backwards, for Qt::Key_Backtab)
785 QListWidgetItem *nextHeader = nullptr;
786
787 // when iterating forward, start at the currentindex, when backwards,
788 // one up from our header, or at the end
789 int j;
790 if (keyEvent->key() == Qt::Key_Tab) {
791 j = currentIndex;
792 } else {
793 index = myHeaderIndex;
794 if (index == 0) {
795 j = completionBox()->count() - 1;
796 } else {
797 j = (index - 1) % completionBox()->count();
798 }
799 }
800 while ((nextHeader = completionBox()->item(j)) && nextHeader != myHeader) {
801 if (itemIsHeader(nextHeader)) {
802 break;
803 }
804 j = (j + iterationStep) % completionBox()->count();
805 }
806
807 if (nextHeader && nextHeader != myHeader) {
808 QListWidgetItem *item = completionBox()->item(j + 1);
809 if (item && !itemIsHeader(item)) {
811 item->setSelected(true);
812 }
813 }
814
815 return true;
816 }
817 }
818
819 return KLineEdit::eventFilter(object, event);
820}
821
822void AddresseeLineEdit::emitTextCompleted()
823{
824 Q_EMIT textCompleted();
825}
826
827void AddresseeLineEdit::callUserCancelled(const QString &str)
828{
829 userCancelled(str);
830}
831
832void AddresseeLineEdit::callSetCompletedText(const QString &text, bool marked)
833{
834 setCompletedText(text, marked);
835}
836
837void AddresseeLineEdit::callSetCompletedText(const QString &text)
838{
840}
841
842void AddresseeLineEdit::callSetUserSelection(bool b)
843{
845}
846
847void AddresseeLineEdit::updateBalooBlackList()
848{
849 d->updateBalooBlackList();
850}
851
852void AddresseeLineEdit::updateCompletionOrder()
853{
854 d->updateCompletionOrder();
855}
856
857KLDAPCore::LdapClientSearch *AddresseeLineEdit::ldapSearch() const
858{
859 return d->ldapSearch();
860}
861
862void AddresseeLineEdit::slotEditingFinished()
863{
864 const QList<KJob *> listJob = d->mightBeGroupJobs();
865 for (KJob *job : listJob) {
866 disconnect(job);
867 job->deleteLater();
868 }
869
870 d->mightBeGroupJobsClear();
871 d->groupsClear();
872
873 if (!text().trimmed().isEmpty() && enableAkonadiSearch()) {
875 for (const QString &address : addresses) {
876 auto job = new Akonadi::ContactGroupSearchJob();
877 connect(job, &Akonadi::ContactGroupSearchJob::result, this, &AddresseeLineEdit::slotGroupSearchResult);
878 d->mightBeGroupJobsAdd(job);
879 job->setQuery(Akonadi::ContactGroupSearchJob::Name, address);
880 }
881 }
882}
883
884void AddresseeLineEdit::slotGroupSearchResult(KJob *job)
885{
887
888 // Laurent I don't understand why Akonadi::ContactGroupSearchJob send two "result(...)" signal. For the moment
889 // avoid to go in this method twice, until I understand it.
890 if (!d->mightBeGroupJobs().contains(searchJob)) {
891 return;
892 }
893 // Q_ASSERT(d->mMightBeGroupJobs.contains(searchJob));
894 d->mightBeGroupJobsRemoveOne(searchJob);
895
896 const KContacts::ContactGroup::List contactGroups = searchJob->contactGroups();
897 if (contactGroups.isEmpty()) {
898 return; // Nothing todo, probably a normal email address was entered
899 }
900
901 d->addGroups(contactGroups);
902 searchJob->deleteLater();
903
904 if (d->autoGroupExpand()) {
905 expandGroups();
906 }
907}
908
909void AddresseeLineEdit::expandGroups()
910{
912
913 const KContacts::ContactGroup::List lstGroups = d->groups();
914 for (const KContacts::ContactGroup &group : lstGroups) {
915 auto expandJob = new Akonadi::ContactGroupExpandJob(group);
916 connect(expandJob, &Akonadi::ContactGroupExpandJob::result, this, &AddresseeLineEdit::groupExpandResult);
917 addresses.removeAll(group.name());
918 expandJob->start();
919 }
920 setText(addresses.join(", "_L1));
921 d->groupsClear();
922}
923
924void AddresseeLineEdit::setRecentAddressConfig(KConfig *config)
925{
926 d->setRecentAddressConfig(config);
927}
928
929KConfig *AddresseeLineEdit::recentAddressConfig() const
930{
931 return d->recentAddressConfig();
932}
933
934#include "moc_addresseelineedit.cpp"
bool hasPayload() const
T payload() const
void setNameFromString(const QString &s)
void addEmail(const Email &email)
QString familyName() const
QStringList emails() const
QString nickName() const
AddresseeList List
QString fullEmail(const QString &email=QString()) const
QString givenName() const
QString name() const
Addressee::List parseVCards(const QByteArray &vcard) const
void result(KJob *job)
bool event(QEvent *) override
void setCompletedText(const QString &) override
QMenu * createStandardContextMenu()
virtual void setText(const QString &)
void keyPressEvent(QKeyEvent *) override
void setUserSelection(bool userSelection)
void mouseReleaseEvent(QMouseEvent *) override
void setCompletionModeDisabled(KCompletion::CompletionMode mode, bool disable=true)
virtual KCompletionBox * completionBox(bool create=true)
void userCancelled(const QString &cancelText)
static QString removeAcceleratorMarker(const QString &label)
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
The CompletionConfigureDialog class.
static RecentAddresses * self(KConfig *config=nullptr)
void clear()
Removes all entries from the history.
KCODECS_EXPORT QString normalizedAddress(const QString &displayName, const QString &addrSpec, const QString &comment=QString())
KCODECS_EXPORT QString quoteNameIfNecessary(const QString &str)
KCODECS_EXPORT bool extractEmailAddressAndName(const QString &aStr, QString &mail, QString &name)
KCODECS_EXPORT QString normalizeAddressesAndDecodeIdn(const QString &addresses)
KCODECS_EXPORT QStringList splitAddressList(const QString &aStr)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString fullName(const PartType &type)
KCODECS_EXPORT QString decodeRFC2047String(QByteArrayView src, QByteArray *usedCS, const QByteArray &defaultCS=QByteArray(), CharsetOption option=NoOption)
KCONTACTS_EXPORT bool convertFromXml(QIODevice *device, ContactGroup &group, QString *errorMessage=nullptr)
KCONTACTS_EXPORT bool canDecode(const QMimeData *md)
KCONTACTS_EXPORT bool fromMimeData(const QMimeData *md, KContacts::Addressee::List &contacts)
KCODECS_EXPORT QString decodeMailtoUrl(const QUrl &mailtoUrl)
QString name(GameStandardAction id)
KIOCORE_EXPORT StoredTransferJob * storedGet(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
PostalAddress address(const QVariant &location)
void setWindow(QObject *job, QWidget *widget)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const QList< QKeySequence > & shortcut(StandardShortcut id)
folderdialogacltab.h
void setCheckable(bool)
void setChecked(bool)
void triggered(bool checked)
char * data()
bool isSpace(char32_t ucs4)
bool supportsSelection() const const
QPoint pos()
MouseButtonPress
QClipboard * clipboard()
virtual void dropEvent(QDropEvent *e) override
void insert(const QString &newText)
void setModified(bool)
void paste()
bool isReadOnly() const const
int selectionStart() const const
void append(QList< T > &&value)
qsizetype count() const const
bool empty() const const
reference front()
bool isEmpty() const const
qsizetype removeAll(const AT &t)
qsizetype size() const const
QListWidgetItem * item(int row) const const
QListWidgetItem * itemAt(const QPoint &p) const const
void scrollToItem(const QListWidgetItem *item, QAbstractItemView::ScrollHint hint)
void setCurrentItem(QListWidgetItem *item)
void setSelected(bool select)
QString text() const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSeparator()
QAction * exec()
bool hasText() const const
bool hasUrls() const const
QString text() const const
QList< QUrl > urls() const const
QPoint pos() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
virtual bool eventFilter(QObject *watched, QEvent *event)
T findChild(const QString &name, Qt::FindChildOptions options) const const
T qobject_cast(QObject *object)
Qt::MouseButtons buttons() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
void chop(qsizetype n)
void clear()
bool isEmpty() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString trimmed() const const
void truncate(qsizetype position)
QString join(QChar separator) const const
MiddleButton
void keyEvent(KeyAction action, QWidget *widget, Qt::Key key, Qt::KeyboardModifiers modifier, int delay)
QString fromPercentEncoding(const QByteArray &input)
void setFont(const QFont &)
QWidget * parentWidget() const const
bool isVisible() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:39:22 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.