File indexing completed on 2025-01-05 04:58:18

0001 /*
0002   This file is part of libkdepim.
0003 
0004   SPDX-FileCopyrightText: 2002 Helge Deller <deller@gmx.de>
0005   SPDX-FileCopyrightText: 2002 Lubos Lunak <llunak@suse.cz>
0006   SPDX-FileCopyrightText: 2001, 2003 Carsten Pfeiffer <pfeiffer@kde.org>
0007   SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
0008   SPDX-FileCopyrightText: 2004 Daniel Molkentin <danimo@klaralvdalens-datakonsult.se>
0009   SPDX-FileCopyrightText: 2004 Karl-Heinz Zimmer <khz@klaralvdalens-datakonsult.se>
0010   SPDX-FileCopyrightText: 2017-2024 Laurent Montel <montel@kde.org>
0011 
0012   SPDX-License-Identifier: LGPL-2.0-or-later
0013 */
0014 
0015 #include "addresseelineedit.h"
0016 #include "addresseelineedit_p.h"
0017 #include "addresseelineeditmanager.h"
0018 #include "addresseelineeditutil.h"
0019 #include "addressline/recentaddress/recentaddresses.h"
0020 #include <KLDAPWidgets/LdapClientSearch>
0021 
0022 #include <KContacts/VCardConverter>
0023 
0024 #include <Akonadi/Job>
0025 #include <KConfigGroup>
0026 #include <QUrl>
0027 
0028 #include <Akonadi/ContactGroupExpandJob>
0029 #include <Akonadi/ContactGroupSearchJob>
0030 #include <KColorScheme>
0031 #include <KContacts/ContactGroupTool>
0032 #include <KEmailAddress>
0033 #include <KIO/StoredTransferJob>
0034 #include <KJobWidgets>
0035 
0036 #include "pimcommonakonadi_debug.h"
0037 #include <KCodecs>
0038 #include <KCompletionBox>
0039 #include <KLocalizedString>
0040 #include <KStandardShortcut>
0041 
0042 #include "addressline/completionconfiguredialog/completionconfiguredialog.h"
0043 #include <Akonadi/ContactGroupExpandJob>
0044 #include <KContacts/VCardDrag>
0045 #include <KMessageBox>
0046 #include <KSharedConfig>
0047 #include <QApplication>
0048 #include <QBuffer>
0049 #include <QClipboard>
0050 #include <QDropEvent>
0051 #include <QEvent>
0052 #include <QKeyEvent>
0053 #include <QMenu>
0054 #include <QMimeData>
0055 #include <QMouseEvent>
0056 #include <QObject>
0057 
0058 using namespace PimCommon;
0059 
0060 inline bool itemIsHeader(const QListWidgetItem *item)
0061 {
0062     return item && !item->text().startsWith(QLatin1StringView("     "));
0063 }
0064 
0065 // needs to be unique, but the actual name doesn't matter much
0066 static QString newLineEditObjectName()
0067 {
0068     static int s_count = 0;
0069     QString name(QStringLiteral("KPIM::AddresseeLineEdit"));
0070     if (s_count++) {
0071         name += QLatin1Char('-');
0072         name += QString::number(s_count);
0073     }
0074     return name;
0075 }
0076 
0077 AddresseeLineEdit::AddresseeLineEdit(QWidget *parent, bool enableCompletion)
0078     : KLineEdit(parent)
0079     , d(new AddresseeLineEditPrivate(this, enableCompletion))
0080 {
0081     setObjectName(newLineEditObjectName());
0082     setPlaceholderText(QString());
0083 
0084     d->init();
0085 }
0086 
0087 AddresseeLineEdit::~AddresseeLineEdit()
0088 {
0089     delete d;
0090 }
0091 
0092 void AddresseeLineEdit::setFont(const QFont &font)
0093 {
0094     KLineEdit::setFont(font);
0095 
0096     if (d->useCompletion()) {
0097         completionBox()->setFont(font);
0098     }
0099 }
0100 
0101 void AddresseeLineEdit::setIcon(const QIcon &icon, const QString &tooltip)
0102 {
0103     d->setIcon(icon, tooltip);
0104 }
0105 
0106 bool AddresseeLineEdit::expandIntern() const
0107 {
0108     return d->expandIntern();
0109 }
0110 
0111 void AddresseeLineEdit::setExpandIntern(bool expand)
0112 {
0113     d->setExpandIntern(expand);
0114 }
0115 
0116 void AddresseeLineEdit::setEnableBalooSearch(bool enable)
0117 {
0118     d->setEnableBalooSearch(enable);
0119 }
0120 
0121 bool AddresseeLineEdit::enableBalooSearch() const
0122 {
0123     return d->enableAkonadiSearch();
0124 }
0125 
0126 void AddresseeLineEdit::setEnableAkonadiSearch(bool enable)
0127 {
0128     d->setEnableAkonadiSearch(enable);
0129 }
0130 
0131 bool AddresseeLineEdit::enableAkonadiSearch() const
0132 {
0133     return d->enableAkonadiSearch();
0134 }
0135 
0136 void AddresseeLineEdit::allowSemicolonAsSeparator(bool useSemicolonAsSeparator)
0137 {
0138     d->setUseSemicolonAsSeparator(useSemicolonAsSeparator);
0139 }
0140 
0141 bool AddresseeLineEdit::showRecentAddresses() const
0142 {
0143     return d->showRecentAddresses();
0144 }
0145 
0146 void AddresseeLineEdit::setShowRecentAddresses(bool b)
0147 {
0148     d->setShowRecentAddresses(b);
0149 }
0150 
0151 void AddresseeLineEdit::keyPressEvent(QKeyEvent *event)
0152 {
0153     bool accept = false;
0154 
0155     const int key = event->key() | event->modifiers();
0156 
0157     if (KStandardShortcut::shortcut(KStandardShortcut::SubstringCompletion).contains(key)) {
0158         // TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch
0159         d->updateSearchString();
0160         d->startSearches();
0161         d->doCompletion(true);
0162         accept = true;
0163     } else if (KStandardShortcut::shortcut(KStandardShortcut::TextCompletion).contains(key)) {
0164         const int len = text().length();
0165 
0166         if (len == cursorPosition()) { // at End?
0167             d->updateSearchString();
0168             d->startSearches();
0169             d->doCompletion(true);
0170             accept = true;
0171         }
0172     }
0173 
0174     const QString oldContent = text();
0175     if (!accept) {
0176         KLineEdit::keyPressEvent(event);
0177     }
0178 
0179     // if the text didn't change (eg. because a cursor navigation key was pressed)
0180     // we don't need to trigger a new search
0181     if (oldContent == text()) {
0182         return;
0183     }
0184 
0185     if (event->isAccepted()) {
0186         d->updateSearchString();
0187 
0188         QString searchString(d->searchString());
0189         // LDAP does not know about our string manipulation, remove it
0190         if (d->searchExtended()) {
0191             searchString = d->searchString().mid(1);
0192         }
0193 
0194         d->restartTime(searchString);
0195     }
0196 }
0197 
0198 void AddresseeLineEdit::insert(const QString &t)
0199 {
0200     if (!d->smartPaste()) {
0201         KLineEdit::insert(t);
0202         return;
0203     }
0204 
0205     QString newText = t.trimmed();
0206     if (newText.isEmpty()) {
0207         return;
0208     }
0209 
0210     newText = PimCommon::AddresseeLineEditUtil::adaptPasteMails(newText);
0211 
0212     QString contents = text();
0213     int pos = cursorPosition();
0214 
0215     if (hasSelectedText()) {
0216         // Cut away the selection.
0217         int start_sel = selectionStart();
0218         pos = start_sel;
0219         contents = contents.left(start_sel) + contents.mid(start_sel + selectedText().length());
0220     }
0221 
0222     int eot = contents.length();
0223     while ((eot > 0) && contents.at(eot - 1).isSpace()) {
0224         --eot;
0225     }
0226     if (eot == 0) {
0227         contents.clear();
0228     } else if (pos >= eot) {
0229         if (contents.at(eot - 1) == QLatin1Char(',')) {
0230             --eot;
0231         }
0232         contents.truncate(eot);
0233         contents += QStringLiteral(", ");
0234         pos = eot + 2;
0235     }
0236 
0237     contents = contents.left(pos) + newText + contents.mid(pos);
0238     setText(contents);
0239     setModified(true);
0240     setCursorPosition(pos + newText.length());
0241 }
0242 
0243 void AddresseeLineEdit::setText(const QString &text)
0244 {
0245     const int cursorPos = cursorPosition();
0246     KLineEdit::setText(text.trimmed());
0247     setCursorPosition(cursorPos);
0248 }
0249 
0250 void AddresseeLineEdit::paste()
0251 {
0252     if (d->useCompletion()) {
0253         d->setSmartPaste(true);
0254     }
0255 
0256     KLineEdit::paste();
0257     d->setSmartPaste(false);
0258 }
0259 
0260 void AddresseeLineEdit::mouseReleaseEvent(QMouseEvent *event)
0261 {
0262     // reimplemented from QLineEdit::mouseReleaseEvent()
0263 #ifndef QT_NO_CLIPBOARD
0264     if (d->useCompletion() && QApplication::clipboard()->supportsSelection() && !isReadOnly() && event->button() == Qt::MiddleButton) {
0265         d->setSmartPaste(true);
0266     }
0267 #endif
0268 
0269     KLineEdit::mouseReleaseEvent(event);
0270     d->setSmartPaste(false);
0271 }
0272 
0273 #ifndef QT_NO_DRAGANDDROP
0274 void AddresseeLineEdit::dropEvent(QDropEvent *event)
0275 {
0276     const QMimeData *md = event->mimeData();
0277     // Case one: The user dropped a text/directory (i.e. vcard), so decode its
0278     //           contents
0279     if (KContacts::VCardDrag::canDecode(md)) {
0280         KContacts::Addressee::List list;
0281         KContacts::VCardDrag::fromMimeData(md, list);
0282 
0283         for (const KContacts::Addressee &addr : std::as_const(list)) {
0284             insertEmails(addr.emails());
0285         }
0286     }
0287     // Case two: The user dropped a list or Urls.
0288     // Iterate over that list. For mailto: Urls, just add the addressee to the list,
0289     // and for other Urls, download the Url and assume it points to a vCard
0290     else if (md->hasUrls()) {
0291         const QList<QUrl> urls = md->urls();
0292         KContacts::Addressee::List list;
0293 
0294         for (const QUrl &url : urls) {
0295             // First, let's deal with mailto Urls. The path() part contains the
0296             // email-address.
0297             if (url.scheme() == QLatin1StringView("mailto")) {
0298                 KContacts::Addressee addressee;
0299                 KContacts::Email email(KEmailAddress::decodeMailtoUrl(url));
0300                 email.setPreferred(true);
0301                 addressee.addEmail(email);
0302                 list += addressee;
0303             } else { // Otherwise, download the vCard to which the Url points
0304                 KContacts::VCardConverter converter;
0305                 auto job = KIO::storedGet(url);
0306                 KJobWidgets::setWindow(job, parentWidget());
0307                 if (job->exec()) {
0308                     QByteArray data = job->data();
0309                     list += converter.parseVCards(data);
0310 
0311                     if (list.isEmpty()) { // try to parse a contact group
0312                         KContacts::ContactGroup group;
0313                         QBuffer dataStream(&data);
0314                         dataStream.open(QIODevice::ReadOnly);
0315                         QString error;
0316                         if (KContacts::ContactGroupTool::convertFromXml(&dataStream, group, &error)) {
0317                             auto expandJob = new Akonadi::ContactGroupExpandJob(group);
0318                             connect(expandJob, &Akonadi::ContactGroupExpandJob::result, this, &AddresseeLineEdit::groupExpandResult);
0319                             expandJob->start();
0320                         } else {
0321                             qCWarning(PIMCOMMONAKONADI_LOG) << "Error during converting contactgroup " << error;
0322                         }
0323                     }
0324                 } else {
0325                     const QString caption(i18n("vCard Import Failed"));
0326                     const QString text = i18n("<qt>Unable to access <b>%1</b>.</qt>", url.url());
0327                     KMessageBox::error(parentWidget(), text, caption);
0328                 }
0329             }
0330         }
0331 
0332         // Now, let the user choose which addressee to add.
0333         for (const KContacts::Addressee &addressee : std::as_const(list)) {
0334             insertEmails(addressee.emails());
0335         }
0336     }
0337     // Case three: Let AddresseeLineEdit deal with the rest
0338     else {
0339         if (!isReadOnly()) {
0340             const QList<QUrl> uriList = event->mimeData()->urls();
0341             if (!uriList.isEmpty()) {
0342                 QString contents = text();
0343                 // remove trailing white space and comma
0344                 int eot = contents.length();
0345                 while ((eot > 0) && contents.at(eot - 1).isSpace()) {
0346                     --eot;
0347                 }
0348                 if (eot == 0) {
0349                     contents.clear();
0350                 } else if (contents.at(eot - 1) == QLatin1Char(',')) {
0351                     --eot;
0352                     contents.truncate(eot);
0353                 }
0354                 bool mailtoURL = false;
0355                 // append the mailto URLs
0356                 for (const QUrl &url : uriList) {
0357                     if (url.scheme() == QLatin1StringView("mailto")) {
0358                         mailtoURL = true;
0359                         QString address;
0360                         address = QUrl::fromPercentEncoding(url.path().toLatin1());
0361                         address = KCodecs::decodeRFC2047String(address);
0362                         if (!contents.isEmpty()) {
0363                             contents.append(QLatin1StringView(", "));
0364                         }
0365                         contents.append(address);
0366                     }
0367                 }
0368                 if (mailtoURL) {
0369                     setText(contents);
0370                     setModified(true);
0371                     return;
0372                 }
0373             } else {
0374                 // Let's see if this drop contains a comma separated list of emails
0375                 if (md->hasText()) {
0376                     const QString dropData = md->text();
0377                     const QStringList addrs = KEmailAddress::splitAddressList(dropData);
0378                     if (!addrs.isEmpty()) {
0379                         if (addrs.count() == 1) {
0380                             QUrl url(dropData);
0381                             if (url.scheme() == QLatin1StringView("mailto")) {
0382                                 KContacts::Addressee addressee;
0383                                 KContacts::Email email(KEmailAddress::decodeMailtoUrl(url));
0384                                 email.setPreferred(true);
0385                                 addressee.addEmail(email);
0386                                 insertEmails(addressee.emails());
0387                             } else {
0388                                 setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData));
0389                             }
0390                         } else {
0391                             setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData));
0392                         }
0393                         setModified(true);
0394                         return;
0395                     }
0396                 }
0397             }
0398         }
0399 
0400         if (d->useCompletion()) {
0401             d->setSmartPaste(true);
0402         }
0403 
0404         QLineEdit::dropEvent(event);
0405         d->setSmartPaste(false);
0406     }
0407 }
0408 
0409 #endif // QT_NO_DRAGANDDROP
0410 
0411 void AddresseeLineEdit::groupExpandResult(KJob *job)
0412 {
0413     auto expandJob = qobject_cast<Akonadi::ContactGroupExpandJob *>(job);
0414 
0415     if (!expandJob) {
0416         return;
0417     }
0418 
0419     const KContacts::Addressee::List contacts = expandJob->contacts();
0420     for (const KContacts::Addressee &addressee : contacts) {
0421         if (d->expandIntern() || text().trimmed().isEmpty()) {
0422             insertEmails({addressee.fullEmail()});
0423         } else {
0424             Q_EMIT addAddress(addressee.fullEmail());
0425         }
0426     }
0427 
0428     job->deleteLater();
0429 }
0430 
0431 void AddresseeLineEdit::insertEmails(const QStringList &emails)
0432 {
0433     if (emails.empty()) {
0434         return;
0435     }
0436 
0437     QString contents = text();
0438     if (!contents.isEmpty()) {
0439         contents += QLatin1Char(',');
0440     }
0441     // only one address, don't need kpopup to choose
0442     if (emails.size() == 1) {
0443         setText(contents + emails.front());
0444         return;
0445     }
0446     // multiple emails, let the user choose one
0447     QMenu menu(this);
0448     menu.setTitle(i18n("Select email from contact"));
0449     menu.setObjectName(QLatin1StringView("Addresschooser"));
0450     for (const QString &email : emails) {
0451         menu.addAction(email);
0452     }
0453     const QAction *result = menu.exec(QCursor::pos());
0454     if (!result) {
0455         return;
0456     }
0457     setText(contents + KLocalizedString::removeAcceleratorMarker(result->text()));
0458 }
0459 
0460 void AddresseeLineEdit::cursorAtEnd()
0461 {
0462     setCursorPosition(text().length());
0463 }
0464 
0465 void AddresseeLineEdit::enableCompletion(bool enable)
0466 {
0467     d->setUseCompletion(enable);
0468 }
0469 
0470 bool AddresseeLineEdit::isCompletionEnabled() const
0471 {
0472     return d->useCompletion();
0473 }
0474 
0475 void AddresseeLineEdit::addItem(const Akonadi::Item &item, int weight, int source)
0476 {
0477     // Let Akonadi results always have a higher weight than baloo results
0478     if (item.hasPayload<KContacts::Addressee>()) {
0479         addContact(item.payload<KContacts::Addressee>(), weight + 1, source);
0480     } else if (item.hasPayload<KContacts::ContactGroup>()) {
0481         addContactGroup(item.payload<KContacts::ContactGroup>(), weight + 1, source);
0482     }
0483 }
0484 
0485 void AddresseeLineEdit::addContactGroup(const KContacts::ContactGroup &group, int weight, int source)
0486 {
0487     d->addCompletionItem(group.name(), weight, source);
0488 }
0489 
0490 void AddresseeLineEdit::addContact(const QStringList &emails, const KContacts::Addressee &addr, int weight, int source, QString append)
0491 {
0492     int isPrefEmail = 1; // first in list is preferredEmail
0493     for (const QString &email : emails) {
0494         // TODO: highlight preferredEmail
0495         const QString givenName = addr.givenName();
0496         const QString familyName = addr.familyName();
0497         const QString nickName = addr.nickName();
0498         const QString fullEmail = addr.fullEmail(email);
0499 
0500         QString appendix;
0501 
0502         if (!append.isEmpty()) {
0503             appendix = QStringLiteral(" (%1)");
0504             append.replace(QLatin1Char('('), QStringLiteral("["));
0505             append.replace(QLatin1Char(')'), QStringLiteral("]"));
0506             appendix = appendix.arg(append);
0507         }
0508 
0509         // Prepare "givenName" + ' ' + "familyName"
0510         QString fullName = givenName;
0511         if (!familyName.isEmpty()) {
0512             if (!fullName.isEmpty()) {
0513                 fullName += QLatin1Char(' ');
0514             }
0515             fullName += familyName;
0516         }
0517 
0518         // Finally, we can add the completion items
0519         if (!fullName.isEmpty()) {
0520             const QString address = KEmailAddress::normalizedAddress(fullName, email, QString());
0521             if (fullEmail != address) {
0522                 // This happens when fullEmail contains a middle name, while our own fullName+email only has "first last".
0523                 // Let's offer both, the fullEmail with 3 parts, looks a tad formal.
0524                 d->addCompletionItem(address + appendix, weight + isPrefEmail, source);
0525             }
0526         }
0527 
0528         QStringList keyWords;
0529         if (!nickName.isEmpty()) {
0530             keyWords.append(nickName);
0531         }
0532 
0533         d->addCompletionItem(fullEmail + appendix, weight + isPrefEmail, source, &keyWords);
0534 
0535         isPrefEmail = 0;
0536     }
0537 }
0538 
0539 void AddresseeLineEdit::addContact(const KContacts::Addressee &addr, int weight, int source, const QString &append)
0540 {
0541     const QStringList emails = AddresseeLineEditManager::self()->cleanupEmailList(addr.emails());
0542     if (emails.isEmpty()) {
0543         return;
0544     }
0545     addContact(emails, addr, weight, source, append);
0546 }
0547 
0548 #ifndef QT_NO_CONTEXTMENU
0549 void AddresseeLineEdit::contextMenuEvent(QContextMenuEvent *event)
0550 {
0551     QMenu *menu = createStandardContextMenu();
0552     if (menu) { // can be 0 on platforms with only a touch interface
0553         menu->exec(event->globalPos());
0554         delete menu;
0555     }
0556 }
0557 
0558 QMenu *AddresseeLineEdit::createStandardContextMenu()
0559 {
0560     // disable modes not supported by KMailCompletion
0561     setCompletionModeDisabled(KCompletion::CompletionMan);
0562     setCompletionModeDisabled(KCompletion::CompletionPopupAuto);
0563 
0564     QMenu *menu = KLineEdit::createStandardContextMenu();
0565     if (!menu) {
0566         return nullptr;
0567     }
0568     if (d->useCompletion()) {
0569         auto showOU = new QAction(i18n("Show Organization Unit for LDAP results"), menu);
0570         showOU->setCheckable(true);
0571         showOU->setChecked(d->showOU());
0572         connect(showOU, &QAction::triggered, d, &AddresseeLineEditPrivate::slotShowOUChanged);
0573         menu->addAction(showOU);
0574     }
0575     if (isCompletionEnabled()) {
0576         menu->addSeparator();
0577         QAction *act = menu->addAction(i18n("Configure Completion..."));
0578         connect(act, &QAction::triggered, this, &AddresseeLineEdit::configureCompletion);
0579     }
0580     menu->addSeparator();
0581     QAction *act = menu->addAction(i18n("Automatically expand groups"));
0582     act->setCheckable(true);
0583     act->setChecked(d->autoGroupExpand());
0584     connect(act, &QAction::triggered, d, &AddresseeLineEditPrivate::slotToggleExpandGroups);
0585 
0586     if (!d->groupsIsEmpty()) {
0587         act = menu->addAction(i18n("Expand Groups..."));
0588         connect(act, &QAction::triggered, this, &AddresseeLineEdit::expandGroups);
0589     }
0590     return menu;
0591 }
0592 
0593 #endif
0594 
0595 bool AddresseeLineEdit::canDeleteLineEdit() const
0596 {
0597     return d->canDeleteLineEdit();
0598 }
0599 
0600 void AddresseeLineEdit::configureCompletion()
0601 {
0602     d->setCanDeleteLineEdit(false);
0603     QScopedPointer<PimCommon::CompletionConfigureDialog> dlg(new PimCommon::CompletionConfigureDialog(this));
0604     dlg->setRecentAddresses(PimCommon::RecentAddresses::self(recentAddressConfig())->addresses());
0605     dlg->setLdapClientSearch(ldapSearch());
0606     dlg->setEmailBlackList(PimCommon::AddresseeLineEditManager::self()->balooBlackList());
0607     dlg->load();
0608     if (dlg->exec() && dlg) {
0609         if (dlg->recentAddressWasChanged()) {
0610             PimCommon::RecentAddresses::self(recentAddressConfig())->clear();
0611             dlg->storeAddresses(recentAddressConfig());
0612             loadContacts();
0613         }
0614         updateBalooBlackList();
0615         updateCompletionOrder();
0616     }
0617     d->setCanDeleteLineEdit(true);
0618 }
0619 
0620 void AddresseeLineEdit::loadContacts()
0621 {
0622     const QString recentAddressGroupName = i18n("Recent Addresses");
0623     if (showRecentAddresses()) {
0624         const QStringList recent =
0625             AddresseeLineEditManager::self()->cleanupRecentAddressEmailList(PimCommon::RecentAddresses::self(recentAddressConfig())->addresses());
0626         QString name;
0627         QString emailString;
0628 
0629         KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kpimcompletionorder"));
0630         KConfigGroup group(config, QStringLiteral("CompletionWeights"));
0631         const int weight = group.readEntry("Recent Addresses", 10);
0632         removeCompletionSource(recentAddressGroupName);
0633         const int idx = addCompletionSource(recentAddressGroupName, weight);
0634 
0635         for (const QString &recentAdr : recent) {
0636             KContacts::Addressee addr;
0637             KEmailAddress::extractEmailAddressAndName(recentAdr, emailString, name);
0638             if (emailString.isEmpty()) {
0639                 continue;
0640             }
0641             name = KEmailAddress::quoteNameIfNecessary(name);
0642             if (!name.isEmpty() && (name[0] == QLatin1Char('"')) && (name[name.length() - 1] == QLatin1Char('"'))) {
0643                 name.remove(0, 1);
0644                 name.chop(1);
0645             }
0646             addr.setNameFromString(name);
0647             KContacts::Email email(emailString);
0648             email.setPreferred(true);
0649             addr.addEmail(email);
0650             addContact({emailString}, addr, weight, idx);
0651         }
0652     } else {
0653         removeCompletionSource(recentAddressGroupName);
0654     }
0655 }
0656 
0657 void AddresseeLineEdit::removeCompletionSource(const QString &source)
0658 {
0659     d->removeCompletionSource(source);
0660 }
0661 
0662 int AddresseeLineEdit::addCompletionSource(const QString &source, int weight)
0663 {
0664     return d->addCompletionSource(source, weight);
0665 }
0666 
0667 bool AddresseeLineEdit::eventFilter(QObject *object, QEvent *event)
0668 {
0669     if (d->completionInitialized() && (object == completionBox() || completionBox()->findChild<QWidget *>(object->objectName()) == object)) {
0670         if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease
0671             || event->type() == QEvent::MouseButtonDblClick) {
0672             const QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
0673             // find list box item at the event position
0674             QListWidgetItem *item = completionBox()->itemAt(mouseEvent->pos());
0675             if (!item) {
0676                 // In the case of a mouse move outside of the box we don't want
0677                 // the parent to fuzzy select a header by mistake.
0678                 const bool eat = event->type() == QEvent::MouseMove;
0679                 return eat;
0680             }
0681             // avoid selection of headers on button press, or move or release while
0682             // a button is pressed
0683             const Qt::MouseButtons buttons = mouseEvent->buttons();
0684             if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick || buttons & Qt::LeftButton
0685                 || buttons & Qt::MiddleButton || buttons & Qt::RightButton) {
0686                 if (itemIsHeader(item)) {
0687                     return true; // eat the event, we don't want anything to happen
0688                 } else {
0689                     // if we are not on one of the group heading, make sure the item
0690                     // below or above is selected, not the heading, inadvertedly, due
0691                     // to fuzzy auto-selection from QListBox
0692                     completionBox()->setCurrentItem(item);
0693                     item->setSelected(true);
0694                     if (event->type() == QEvent::MouseMove) {
0695                         return true; // avoid fuzzy selection behavior
0696                     }
0697                 }
0698             }
0699         }
0700     }
0701 
0702     if ((object == this) && (event->type() == QEvent::ShortcutOverride)) {
0703         auto keyEvent = static_cast<QKeyEvent *>(event);
0704         if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Tab) {
0705             keyEvent->accept();
0706             return true;
0707         }
0708     }
0709 
0710     if ((object == this) && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) && completionBox()->isVisible()) {
0711         const QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0712         int currentIndex = completionBox()->currentRow();
0713         if (currentIndex < 0) {
0714             return true;
0715         }
0716         if (keyEvent->key() == Qt::Key_Up) {
0717             // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex;
0718             // figure out if the item we would be moving to is one we want
0719             // to ignore. If so, go one further
0720             const QListWidgetItem *itemAbove = completionBox()->item(currentIndex);
0721             if (itemAbove && itemIsHeader(itemAbove)) {
0722                 // there is a header above is, check if there is even further up
0723                 // and if so go one up, so it'll be selected
0724                 if (currentIndex > 0 && completionBox()->item(currentIndex - 1)) {
0725                     // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1;
0726                     completionBox()->setCurrentRow(currentIndex - 1);
0727                     completionBox()->item(currentIndex - 1)->setSelected(true);
0728                 } else if (currentIndex == 0) {
0729                     // nothing to skip to, let's stay where we are, but make sure the
0730                     // first header becomes visible, if we are the first real entry
0731                     completionBox()->scrollToItem(completionBox()->item(0));
0732                     QListWidgetItem *item = completionBox()->item(currentIndex);
0733                     if (item) {
0734                         if (itemIsHeader(item)) {
0735                             currentIndex++;
0736                             item = completionBox()->item(currentIndex);
0737                         }
0738                         completionBox()->setCurrentItem(item);
0739                         item->setSelected(true);
0740                     }
0741                 }
0742 
0743                 return true;
0744             }
0745         } else if (keyEvent->key() == Qt::Key_Down) {
0746             // same strategy for downwards
0747             // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex;
0748             const QListWidgetItem *itemBelow = completionBox()->item(currentIndex);
0749             if (itemBelow && itemIsHeader(itemBelow)) {
0750                 if (completionBox()->item(currentIndex + 1)) {
0751                     // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1;
0752                     completionBox()->setCurrentRow(currentIndex + 1);
0753                     completionBox()->item(currentIndex + 1)->setSelected(true);
0754                 } else {
0755                     // nothing to skip to, let's stay where we are
0756                     QListWidgetItem *item = completionBox()->item(currentIndex);
0757                     if (item) {
0758                         completionBox()->setCurrentItem(item);
0759                         item->setSelected(true);
0760                     }
0761                 }
0762 
0763                 return true;
0764             }
0765             // special case of the initial selection, which is unfortunately a header.
0766             // Setting it to selected tricks KCompletionBox into not treating is special
0767             // and selecting making it current, instead of the one below.
0768             QListWidgetItem *item = completionBox()->item(currentIndex);
0769             if (item && itemIsHeader(item)) {
0770                 completionBox()->setCurrentItem(item);
0771                 item->setSelected(true);
0772             }
0773         } else if (event->type() == QEvent::KeyRelease && (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab)) {
0774             /// first, find the header of the current section
0775             QListWidgetItem *myHeader = nullptr;
0776             int myHeaderIndex = -1;
0777             const int iterationStep = keyEvent->key() == Qt::Key_Tab ? 1 : -1;
0778             int index = qMin(qMax(currentIndex - iterationStep, 0), completionBox()->count() - 1);
0779             while (index >= 0) {
0780                 if (itemIsHeader(completionBox()->item(index))) {
0781                     myHeader = completionBox()->item(index);
0782                     myHeaderIndex = index;
0783                     break;
0784                 }
0785 
0786                 index--;
0787             }
0788             Q_ASSERT(myHeader); // we should always be able to find a header
0789 
0790             // find the next header (searching backwards, for Qt::Key_Backtab)
0791             QListWidgetItem *nextHeader = nullptr;
0792 
0793             // when iterating forward, start at the currentindex, when backwards,
0794             // one up from our header, or at the end
0795             int j;
0796             if (keyEvent->key() == Qt::Key_Tab) {
0797                 j = currentIndex;
0798             } else {
0799                 index = myHeaderIndex;
0800                 if (index == 0) {
0801                     j = completionBox()->count() - 1;
0802                 } else {
0803                     j = (index - 1) % completionBox()->count();
0804                 }
0805             }
0806             while ((nextHeader = completionBox()->item(j)) && nextHeader != myHeader) {
0807                 if (itemIsHeader(nextHeader)) {
0808                     break;
0809                 }
0810                 j = (j + iterationStep) % completionBox()->count();
0811             }
0812 
0813             if (nextHeader && nextHeader != myHeader) {
0814                 QListWidgetItem *item = completionBox()->item(j + 1);
0815                 if (item && !itemIsHeader(item)) {
0816                     completionBox()->setCurrentItem(item);
0817                     item->setSelected(true);
0818                 }
0819             }
0820 
0821             return true;
0822         }
0823     }
0824 
0825     return KLineEdit::eventFilter(object, event);
0826 }
0827 
0828 void AddresseeLineEdit::emitTextCompleted()
0829 {
0830     Q_EMIT textCompleted();
0831 }
0832 
0833 void AddresseeLineEdit::callUserCancelled(const QString &str)
0834 {
0835     userCancelled(str);
0836 }
0837 
0838 void AddresseeLineEdit::callSetCompletedText(const QString &text, bool marked)
0839 {
0840     setCompletedText(text, marked);
0841 }
0842 
0843 void AddresseeLineEdit::callSetCompletedText(const QString &text)
0844 {
0845     setCompletedText(text);
0846 }
0847 
0848 void AddresseeLineEdit::callSetUserSelection(bool b)
0849 {
0850     setUserSelection(b);
0851 }
0852 
0853 void AddresseeLineEdit::updateBalooBlackList()
0854 {
0855     d->updateBalooBlackList();
0856 }
0857 
0858 void AddresseeLineEdit::updateCompletionOrder()
0859 {
0860     d->updateCompletionOrder();
0861 }
0862 
0863 KLDAPWidgets::LdapClientSearch *AddresseeLineEdit::ldapSearch() const
0864 {
0865     return d->ldapSearch();
0866 }
0867 
0868 void AddresseeLineEdit::slotEditingFinished()
0869 {
0870     const QList<KJob *> listJob = d->mightBeGroupJobs();
0871     for (KJob *job : listJob) {
0872         disconnect(job);
0873         job->deleteLater();
0874     }
0875 
0876     d->mightBeGroupJobsClear();
0877     d->groupsClear();
0878 
0879     if (!text().trimmed().isEmpty() && enableAkonadiSearch()) {
0880         const QStringList addresses = KEmailAddress::splitAddressList(text());
0881         for (const QString &address : addresses) {
0882             auto job = new Akonadi::ContactGroupSearchJob();
0883             connect(job, &Akonadi::ContactGroupSearchJob::result, this, &AddresseeLineEdit::slotGroupSearchResult);
0884             d->mightBeGroupJobsAdd(job);
0885             job->setQuery(Akonadi::ContactGroupSearchJob::Name, address);
0886         }
0887     }
0888 }
0889 
0890 void AddresseeLineEdit::slotGroupSearchResult(KJob *job)
0891 {
0892     auto searchJob = qobject_cast<Akonadi::ContactGroupSearchJob *>(job);
0893 
0894     // Laurent I don't understand why Akonadi::ContactGroupSearchJob send two "result(...)" signal. For the moment
0895     // avoid to go in this method twice, until I understand it.
0896     if (!d->mightBeGroupJobs().contains(searchJob)) {
0897         return;
0898     }
0899     // Q_ASSERT(d->mMightBeGroupJobs.contains(searchJob));
0900     d->mightBeGroupJobsRemoveOne(searchJob);
0901 
0902     const KContacts::ContactGroup::List contactGroups = searchJob->contactGroups();
0903     if (contactGroups.isEmpty()) {
0904         return; // Nothing todo, probably a normal email address was entered
0905     }
0906 
0907     d->addGroups(contactGroups);
0908     searchJob->deleteLater();
0909 
0910     if (d->autoGroupExpand()) {
0911         expandGroups();
0912     }
0913 }
0914 
0915 void AddresseeLineEdit::expandGroups()
0916 {
0917     QStringList addresses = KEmailAddress::splitAddressList(text());
0918 
0919     const KContacts::ContactGroup::List lstGroups = d->groups();
0920     for (const KContacts::ContactGroup &group : lstGroups) {
0921         auto expandJob = new Akonadi::ContactGroupExpandJob(group);
0922         connect(expandJob, &Akonadi::ContactGroupExpandJob::result, this, &AddresseeLineEdit::groupExpandResult);
0923         addresses.removeAll(group.name());
0924         expandJob->start();
0925     }
0926     setText(addresses.join(QLatin1StringView(", ")));
0927     d->groupsClear();
0928 }
0929 
0930 void AddresseeLineEdit::setRecentAddressConfig(KConfig *config)
0931 {
0932     d->setRecentAddressConfig(config);
0933 }
0934 
0935 KConfig *AddresseeLineEdit::recentAddressConfig() const
0936 {
0937     return d->recentAddressConfig();
0938 }
0939 
0940 #include "moc_addresseelineedit.cpp"