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

KDE's Doxygen guidelines are available online.