Pimcommon

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

KDE's Doxygen guidelines are available online.