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"