2 Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
3 Copyright (c) 2009-2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
5 Based on old attendeeeditor.cpp:
6 Copyright (c) 2000,2001 Cornelius Schumacher <schumacher@kde.org>
7 Copyright (C) 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
8 Copyright (c) 2007 Volker Krause <vkrause@kde.org>
10 This library is free software; you can redistribute it and/or modify it
11 under the terms of the GNU Library General Public License as published by
12 the Free Software Foundation; either version 2 of the License, or (at your
13 option) any later version.
15 This library is distributed in the hope that it will be useful, but WITHOUT
16 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
18 License for more details.
20 You should have received a copy of the GNU Library General Public License
21 along with this library; see the file COPYING.LIB. If not, write to the
22 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 #include "incidenceattendee.h"
27 #include "attendeeeditor.h"
28 #include "conflictresolver.h"
29 #include "editorconfig.h"
30 #include "incidencedatetime.h"
31 #include "schedulingdialog.h"
32 #ifdef KDEPIM_MOBILE_UI
33 #include "ui_dialogmoremobile.h"
35 #include "ui_dialogdesktop.h"
38 #include <Akonadi/Contact/ContactGroupExpandJob>
39 #include <Akonadi/Contact/ContactGroupSearchJob>
40 #include <Akonadi/Contact/EmailAddressSelectionDialog>
42 #include <KEmailAddress>
46 #include <KMessageBox>
47 #include <KLocalizedString>
49 using namespace IncidenceEditorNG
;
51 static bool compareAttendees(const KCalCore::Attendee::Ptr
&newAttendee
,
52 const KCalCore::Attendee::Ptr
&originalAttendee
)
54 KCalCore::Attendee::Ptr
originalClone(new KCalCore::Attendee(*originalAttendee
));
56 if (newAttendee
->name() != originalAttendee
->name()) {
57 // What you put into an IncidenceEditorNG::AttendeeLine isn't exactly what you get out.
58 // In rare situations, such as "Doe\, John <john.doe@kde.org>", AttendeeLine will normalize
59 // the name, and set "Doe, John <john.doe@kde.org>" instead.
60 // So, for isDirty() purposes, have that in mind, so we don't return that the editor is dirty
61 // when the user didn't edit anything.
63 QString originalNameNormalized
;
64 KEmailAddress::extractEmailAddressAndName(originalAttendee
->fullName(), dummy
, originalNameNormalized
);
65 originalClone
->setName(originalNameNormalized
);
68 return *newAttendee
== *originalClone
;
71 #ifdef KDEPIM_MOBILE_UI
72 IncidenceAttendee::IncidenceAttendee(QWidget
*parent
, IncidenceDateTime
*dateTime
,
73 Ui::EventOrTodoMore
*ui
)
75 IncidenceAttendee::IncidenceAttendee(QWidget
*parent
, IncidenceDateTime
*dateTime
,
76 Ui::EventOrTodoDesktop
*ui
)
79 mParentWidget(parent
),
80 mAttendeeEditor(new AttendeeEditor
),
81 mConflictResolver(0), mDateTime(dateTime
)
83 setObjectName("IncidenceAttendee");
85 QGridLayout
*layout
= new QGridLayout(mUi
->mAttendeWidgetPlaceHolder
);
86 layout
->setSpacing(0);
87 layout
->addWidget(mAttendeeEditor
);
89 //QT5 mAttendeeEditor->setCompletionMode( KGlobalSettings::self()->completionMode() );
90 mAttendeeEditor
->setFrameStyle(QFrame::Sunken
| QFrame::StyledPanel
);
92 #ifdef KDEPIM_MOBILE_UI
93 mAttendeeEditor
->setDynamicSizeHint(false);
96 connect(mAttendeeEditor
, &AttendeeEditor::countChanged
, this, &IncidenceAttendee::attendeeCountChanged
);
97 connect(mAttendeeEditor
, &AttendeeEditor::editingFinished
, this, &IncidenceAttendee::checkIfExpansionIsNeeded
);
99 mUi
->mOrganizerStack
->setCurrentIndex(0);
101 fillOrganizerCombo();
102 mUi
->mSolveButton
->setEnabled(false);
103 mUi
->mOrganizerLabel
->setVisible(false);
105 mConflictResolver
= new ConflictResolver(parent
, parent
);
106 mConflictResolver
->setEarliestDate(mDateTime
->startDate());
107 mConflictResolver
->setEarliestTime(mDateTime
->startTime());
108 mConflictResolver
->setLatestDate(mDateTime
->endDate());
109 mConflictResolver
->setLatestTime(mDateTime
->endTime());
111 connect(mUi
->mSelectButton
, &QPushButton::clicked
, this, &IncidenceAttendee::slotSelectAddresses
);
112 connect(mUi
->mSolveButton
, &QPushButton::clicked
, this, &IncidenceAttendee::slotSolveConflictPressed
);
113 /* Added as part of kolab/issue2297, which is currently under review
114 connect(mUi->mOrganizerCombo, static_cast<void (KComboBox::*)(const QString &)>(&KComboBox::activated), this, &IncidenceAttendee::slotOrganizerChanged);
116 connect(mUi
->mOrganizerCombo
, static_cast<void (KComboBox::*)(int)>(&KComboBox::currentIndexChanged
), this, &IncidenceAttendee::checkDirtyStatus
);
118 connect(mDateTime
, &IncidenceDateTime::startDateChanged
, this, &IncidenceAttendee::slotEventDurationChanged
);
119 connect(mDateTime
, &IncidenceDateTime::endDateChanged
, this, &IncidenceAttendee::slotEventDurationChanged
);
120 connect(mDateTime
, &IncidenceDateTime::startTimeChanged
, this, &IncidenceAttendee::slotEventDurationChanged
);
121 connect(mDateTime
, &IncidenceDateTime::endTimeChanged
, this, &IncidenceAttendee::slotEventDurationChanged
);
123 connect(mConflictResolver
, &ConflictResolver::conflictsDetected
, this, &IncidenceAttendee::slotUpdateConflictLabel
);
125 slotUpdateConflictLabel(0); //initialize label
127 connect(mAttendeeEditor
, &AttendeeEditor::editingFinished
, this, &IncidenceAttendee::checkIfExpansionIsNeeded
);
128 connect(mAttendeeEditor
, &AttendeeEditor::changed
, this, &IncidenceAttendee::slotAttendeeChanged
);
131 IncidenceAttendee::~IncidenceAttendee() {}
133 void IncidenceAttendee::load(const KCalCore::Incidence::Ptr
&incidence
)
135 mLoadedIncidence
= incidence
;
137 if (iAmOrganizer() || incidence
->organizer()->isEmpty()) {
138 mUi
->mOrganizerStack
->setCurrentIndex(0);
141 const QString fullOrganizer
= incidence
->organizer()->fullName();
142 const QString organizerEmail
= incidence
->organizer()->email();
143 for (int i
= 0; i
< mUi
->mOrganizerCombo
->count(); ++i
) {
144 KCalCore::Person::Ptr organizerCandidate
=
145 KCalCore::Person::fromFullName(mUi
->mOrganizerCombo
->itemText(i
));
146 if (organizerCandidate
->email() == organizerEmail
) {
148 mUi
->mOrganizerCombo
->setCurrentIndex(i
);
152 if (found
< 0 && !fullOrganizer
.isEmpty()) {
153 mUi
->mOrganizerCombo
->insertItem(0, fullOrganizer
);
154 mUi
->mOrganizerCombo
->setCurrentIndex(0);
157 mUi
->mOrganizerLabel
->setVisible(false);
158 } else { // someone else is the organizer
159 mUi
->mOrganizerStack
->setCurrentIndex(1);
160 mUi
->mOrganizerLabel
->setText(incidence
->organizer()->fullName());
161 mUi
->mOrganizerLabel
->setVisible(true);
164 mAttendeeEditor
->clear();
165 // NOTE: Do this *before* adding the attendees, otherwise the status of the
166 // attendee in the line will be 0 after when returning from load()
167 if (incidence
->type() == KCalCore::Incidence::TypeEvent
) {
168 mAttendeeEditor
->setActions(AttendeeLine::EventActions
);
170 mAttendeeEditor
->setActions(AttendeeLine::TodoActions
);
173 const KCalCore::Attendee::List attendees
= incidence
->attendees();
174 foreach (const KCalCore::Attendee::Ptr
&a
, attendees
) {
175 mAttendeeEditor
->addAttendee(a
);
181 void IncidenceAttendee::save(const KCalCore::Incidence::Ptr
&incidence
)
183 incidence
->clearAttendees();
184 AttendeeData::List attendees
= mAttendeeEditor
->attendees();
186 foreach (AttendeeData::Ptr attendee
, attendees
) {
190 if (KEmailAddress::isValidAddress(attendee
->email())) {
191 if (KMessageBox::warningYesNo(
194 "%1 does not look like a valid email address. "
195 "Are you sure you want to invite this participant?",
197 i18nc("@title:window", "Invalid Email Address")) != KMessageBox::Yes
) {
202 incidence
->addAttendee(attendee
);
206 // Must not have an organizer for items without attendees
207 if (!incidence
->attendeeCount()) {
211 if (mUi
->mOrganizerStack
->currentIndex() == 0) {
212 incidence
->setOrganizer(mUi
->mOrganizerCombo
->currentText());
214 incidence
->setOrganizer(mUi
->mOrganizerLabel
->text());
218 bool IncidenceAttendee::isDirty() const
220 if (iAmOrganizer()) {
222 tmp
.setOrganizer(mUi
->mOrganizerCombo
->currentText());
224 if (mLoadedIncidence
->organizer()->email() != tmp
.organizer()->email()) {
225 qDebug() << "Organizer changed. Old was " << mLoadedIncidence
->organizer()->name()
226 << mLoadedIncidence
->organizer()->email() << "; new is " << tmp
.organizer()->name()
227 << tmp
.organizer()->email();
232 const KCalCore::Attendee::List originalList
= mLoadedIncidence
->attendees();
233 AttendeeData::List newList
= mAttendeeEditor
->attendees();
235 // The lists sizes *must* be the same. When the organizer is attending the
236 // event as well, he should be in the attendees list as well.
237 if (originalList
.size() != newList
.size()) {
241 // Okay, again not the most efficient algorithm, but I'm assuming that in the
242 // bulk of the use cases, the number of attendees is not much higher than 10 or so.
243 foreach (const KCalCore::Attendee::Ptr
&attendee
, originalList
) {
245 for (int i
= 0; i
< newList
.size(); ++i
) {
246 if (compareAttendees(newList
.at(i
)->attendee(), attendee
)) {
254 // One of the attendees in the original list was not found in the new list.
262 void IncidenceAttendee::changeStatusForMe(KCalCore::Attendee::PartStat stat
)
264 const IncidenceEditorNG::EditorConfig
*config
= IncidenceEditorNG::EditorConfig::instance();
267 AttendeeData::List attendees
= mAttendeeEditor
->attendees();
268 mAttendeeEditor
->clear();
270 foreach (const AttendeeData::Ptr
&attendee
, attendees
) {
271 if (config
->thatIsMe(attendee
->email())) {
272 attendee
->setStatus(stat
);
274 mAttendeeEditor
->addAttendee(attendee
);
280 void IncidenceAttendee::acceptForMe()
282 changeStatusForMe(KCalCore::Attendee::Accepted
);
285 void IncidenceAttendee::declineForMe()
287 changeStatusForMe(KCalCore::Attendee::Declined
);
290 void IncidenceAttendee::fillOrganizerCombo()
292 mUi
->mOrganizerCombo
->clear();
293 const QStringList lst
= IncidenceEditorNG::EditorConfig::instance()->fullEmails();
294 QStringList uniqueList
;
295 for (QStringList::ConstIterator it
= lst
.begin(); it
!= lst
.end(); ++it
) {
296 if (!uniqueList
.contains(*it
)) {
300 mUi
->mOrganizerCombo
->addItems(uniqueList
);
303 void IncidenceAttendee::checkIfExpansionIsNeeded(KPIM::MultiplyingLine
*line
)
305 AttendeeData::Ptr data
= qSharedPointerDynamicCast
<AttendeeData
>(line
->data());
307 qDebug() << "dynamic cast failed";
311 // For some reason, when pressing enter (instead of tab) the editingFinished()
312 // signal is emitted twice. Check if there is already a job running to prevent
313 // that we end up with the group members twice.
314 if (mMightBeGroupLines
.key(QWeakPointer
<KPIM::MultiplyingLine
>(line
)) != 0) {
318 Akonadi::ContactGroupSearchJob
*job
= new Akonadi::ContactGroupSearchJob();
319 job
->setQuery(Akonadi::ContactGroupSearchJob::Name
, data
->name());
320 connect(job
, &Akonadi::ContactGroupSearchJob::result
, this, &IncidenceAttendee::groupSearchResult
);
322 mMightBeGroupLines
.insert(job
, QWeakPointer
<KPIM::MultiplyingLine
>(line
));
325 void IncidenceAttendee::expandResult(KJob
*job
)
327 Akonadi::ContactGroupExpandJob
*expandJob
= qobject_cast
<Akonadi::ContactGroupExpandJob
*>(job
);
330 const KContacts::Addressee::List groupMembers
= expandJob
->contacts();
331 foreach (const KContacts::Addressee
&member
, groupMembers
) {
332 insertAttendeeFromAddressee(member
);
336 void IncidenceAttendee::groupSearchResult(KJob
*job
)
338 Akonadi::ContactGroupSearchJob
*searchJob
= qobject_cast
<Akonadi::ContactGroupSearchJob
*>(job
);
341 Q_ASSERT(mMightBeGroupLines
.contains(job
));
342 KPIM::MultiplyingLine
*line
= mMightBeGroupLines
.take(job
).data();
344 const KContacts::ContactGroup::List contactGroups
= searchJob
->contactGroups();
345 if (contactGroups
.isEmpty()) {
346 return; // Nothing todo, probably a normal email address was entered
349 // TODO: Give the user the possibility to choose a group when there is more than one?!
350 KContacts::ContactGroup group
= contactGroups
.first();
352 line
->slotPropagateDeletion();
355 Akonadi::ContactGroupExpandJob
*expandJob
= new Akonadi::ContactGroupExpandJob(group
, this);
356 connect(expandJob
, &Akonadi::ContactGroupExpandJob::result
, this, &IncidenceAttendee::expandResult
);
360 void IncidenceAttendee::slotSelectAddresses()
362 #ifndef KDEPIM_MOBILE_UI
363 QWidget
*dialogParent
= mAttendeeEditor
;
365 QWidget
*dialogParent
= 0;
367 QWeakPointer
<Akonadi::EmailAddressSelectionDialog
> dialog(
368 new Akonadi::EmailAddressSelectionDialog(dialogParent
));
369 dialog
.data()->view()->view()->setSelectionMode(QAbstractItemView::ExtendedSelection
);
371 if (dialog
.data()->exec() == QDialog::Accepted
) {
373 Akonadi::EmailAddressSelectionDialog
*dialogPtr
= dialog
.data();
375 const Akonadi::EmailAddressSelection::List list
= dialogPtr
->selectedAddresses();
376 foreach (const Akonadi::EmailAddressSelection
&selection
, list
) {
378 if (selection
.item().hasPayload
<KContacts::ContactGroup
>()) {
379 Akonadi::ContactGroupExpandJob
*job
=
380 new Akonadi::ContactGroupExpandJob(
381 selection
.item().payload
<KContacts::ContactGroup
>(), this);
382 connect(job
, &Akonadi::ContactGroupExpandJob::result
, this, &IncidenceAttendee::expandResult
);
385 KContacts::Addressee contact
;
386 contact
.setName(selection
.name());
387 contact
.insertEmail(selection
.email());
389 if (selection
.item().hasPayload
<KContacts::Addressee
>()) {
390 contact
.setUid(selection
.item().payload
<KContacts::Addressee
>().uid());
392 insertAttendeeFromAddressee(contact
);
396 qDebug() << "dialog was already deleted";
401 dialog
.data()->deleteLater();
405 void IncidenceEditorNG::IncidenceAttendee::slotSolveConflictPressed()
407 const int duration
= mDateTime
->startTime().secsTo(mDateTime
->endTime());
408 QScopedPointer
<SchedulingDialog
> dialog(new SchedulingDialog(mDateTime
->startDate(),
409 mDateTime
->startTime(),
410 duration
, mConflictResolver
,
412 dialog
->slotUpdateIncidenceStartEnd(mDateTime
->currentStartDateTime(),
413 mDateTime
->currentEndDateTime());
414 if (dialog
->exec() == QDialog::Accepted
) {
415 qDebug() << dialog
->selectedStartDate() << dialog
->selectedStartTime();
416 mDateTime
->setStartDate(dialog
->selectedStartDate());
417 mDateTime
->setStartTime(dialog
->selectedStartTime());
421 void IncidenceAttendee::slotAttendeeChanged(const KCalCore::Attendee::Ptr
&oldAttendee
,
422 const KCalCore::Attendee::Ptr
&newAttendee
)
424 // if newAttendee's email is empty, we are probably removing an attendee
425 if (mConflictResolver
->containsAttendee(oldAttendee
)) {
426 mConflictResolver
->removeAttendee(oldAttendee
);
428 if (!mConflictResolver
->containsAttendee(newAttendee
) && !newAttendee
->email().isEmpty()) {
429 mConflictResolver
->insertAttendee(newAttendee
);
434 void IncidenceAttendee::slotUpdateConflictLabel(int count
)
436 if (mAttendeeEditor
->attendees().count() > 0) {
437 mUi
->mSolveButton
->setEnabled(true);
439 QString label
= i18ncp("@label Shows the number of scheduling conflicts",
441 "%1 conflicts", count
);
442 mUi
->mConflictsLabel
->setText(label
);
443 mUi
->mConflictsLabel
->setVisible(true);
445 mUi
->mConflictsLabel
->setVisible(false);
448 mUi
->mSolveButton
->setEnabled(false);
449 mUi
->mConflictsLabel
->setVisible(false);
453 bool IncidenceAttendee::iAmOrganizer() const
455 if (mLoadedIncidence
) {
456 const IncidenceEditorNG::EditorConfig
*config
= IncidenceEditorNG::EditorConfig::instance();
457 return config
->thatIsMe(mLoadedIncidence
->organizer()->email());
463 void IncidenceAttendee::insertAttendeeFromAddressee(const KContacts::Addressee
&a
)
465 const bool sameAsOrganizer
= mUi
->mOrganizerCombo
&&
466 KEmailAddress::compareEmail(a
.preferredEmail(),
467 mUi
->mOrganizerCombo
->currentText(),
469 KCalCore::Attendee::PartStat partStat
= KCalCore::Attendee::NeedsAction
;
472 if (iAmOrganizer() && sameAsOrganizer
) {
473 partStat
= KCalCore::Attendee::Accepted
;
476 KCalCore::Attendee::Ptr
newAt(new KCalCore::Attendee(a
.realName(), a
.preferredEmail(),
479 KCalCore::Attendee::ReqParticipant
,
482 mAttendeeEditor
->addAttendee(newAt
);
485 void IncidenceAttendee::slotEventDurationChanged()
487 const KDateTime start
= mDateTime
->currentStartDateTime();
488 const KDateTime end
= mDateTime
->currentEndDateTime();
490 if (start
>= end
) { // This can happen, especially for todos.
494 mConflictResolver
->setEarliestDateTime(start
);
495 mConflictResolver
->setLatestDateTime(end
);
498 void IncidenceAttendee::slotOrganizerChanged(const QString
&newOrganizer
)
500 if (KEmailAddress::compareEmail(newOrganizer
, mOrganizer
, false)) {
506 bool success
= KEmailAddress::extractEmailAddressAndName(newOrganizer
, email
, name
);
509 qWarning() << "Could not extract email address and name";
513 AttendeeData::Ptr currentOrganizerAttendee
;
514 AttendeeData::Ptr newOrganizerAttendee
;
516 Q_FOREACH (AttendeeData::Ptr attendee
, mAttendeeEditor
->attendees()) {
517 if (attendee
->fullName() == mOrganizer
) {
518 currentOrganizerAttendee
= attendee
;
521 if (attendee
->fullName() == newOrganizer
) {
522 newOrganizerAttendee
= attendee
;
526 int answer
= KMessageBox::No
;
527 if (currentOrganizerAttendee
) {
528 answer
= KMessageBox::questionYesNo(
531 "You are changing the organizer of this event. "
532 "Since the organizer is also attending this event, would you "
533 "like to change the corresponding attendee as well?"));
535 answer
= KMessageBox::Yes
;
538 if (answer
== KMessageBox::Yes
) {
539 if (currentOrganizerAttendee
) {
540 mAttendeeEditor
->removeAttendee(currentOrganizerAttendee
);
543 if (!newOrganizerAttendee
) {
544 bool rsvp
= !iAmOrganizer(); // if it is the user, don't make him rsvp.
545 KCalCore::Attendee::PartStat status
= iAmOrganizer() ? KCalCore::Attendee::Accepted
546 : KCalCore::Attendee::NeedsAction
;
548 KCalCore::Attendee::Ptr
newAt(
549 new KCalCore::Attendee(name
, email
, rsvp
, status
, KCalCore::Attendee::ReqParticipant
));
551 mAttendeeEditor
->addAttendee(newAt
);
554 mOrganizer
= newOrganizer
;
557 void IncidenceAttendee::printDebugInfo() const
559 qDebug() << "I'm organizer : " << iAmOrganizer();
560 qDebug() << "Loaded organizer: " << mLoadedIncidence
->organizer()->email();
562 if (iAmOrganizer()) {
564 tmp
.setOrganizer(mUi
->mOrganizerCombo
->currentText());
565 qDebug() << "Organizer combo: " << tmp
.organizer()->email();
568 const KCalCore::Attendee::List originalList
= mLoadedIncidence
->attendees();
569 AttendeeData::List newList
= mAttendeeEditor
->attendees();
570 qDebug() << "List sizes: " << originalList
.count() << newList
.count();
572 if (originalList
.count() != newList
.count()) {
576 // Okay, again not the most efficient algorithm, but I'm assuming that in the
577 // bulk of the use cases, the number of attendees is not much higher than 10 or so.
578 foreach (const KCalCore::Attendee::Ptr
&attendee
, originalList
) {
580 for (int i
= 0; i
< newList
.count(); ++i
) {
581 if (*newList
.at(i
)->attendee() == *attendee
) {
589 qDebug() << "Attendee not found: " << attendee
->email()
591 << attendee
->status()
595 << attendee
->delegate()
596 << attendee
->delegator()
598 for (int i
= 0; i
< newList
.count(); ++i
) {
599 KCalCore::Attendee::Ptr attendee
= newList
.at(i
)->attendee();
600 qDebug() << "Attendee: " << attendee
->email()
602 << attendee
->status()
606 << attendee
->delegate()
607 << attendee
->delegator();