SVN_SILENT made messages (.desktop file)
[kdepim.git] / incidenceeditor-ng / incidenceattendee.cpp
blobaad15731cffdc6e89fa95d7f5653f9dedbfbf26f
1 /*
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
23 02110-1301, USA.
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"
34 #else
35 #include "ui_dialogdesktop.h"
36 #endif
38 #include <Akonadi/Contact/ContactGroupExpandJob>
39 #include <Akonadi/Contact/ContactGroupSearchJob>
40 #include <Akonadi/Contact/EmailAddressSelectionDialog>
42 #include <KEmailAddress>
44 #include <QDebug>
45 #include <QTreeView>
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.
62 QString dummy;
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)
74 #else
75 IncidenceAttendee::IncidenceAttendee(QWidget *parent, IncidenceDateTime *dateTime,
76 Ui::EventOrTodoDesktop *ui)
77 #endif
78 : mUi(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);
94 #endif
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);
140 int found = -1;
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) {
147 found = i;
148 mUi->mOrganizerCombo->setCurrentIndex(i);
149 break;
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);
169 } else {
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);
178 mWasDirty = false;
181 void IncidenceAttendee::save(const KCalCore::Incidence::Ptr &incidence)
183 incidence->clearAttendees();
184 AttendeeData::List attendees = mAttendeeEditor->attendees();
186 foreach (AttendeeData::Ptr attendee, attendees) {
187 Q_ASSERT(attendee);
189 bool skip = false;
190 if (KEmailAddress::isValidAddress(attendee->email())) {
191 if (KMessageBox::warningYesNo(
193 i18nc("@info",
194 "%1 does not look like a valid email address. "
195 "Are you sure you want to invite this participant?",
196 attendee->email()),
197 i18nc("@title:window", "Invalid Email Address")) != KMessageBox::Yes) {
198 skip = true;
201 if (!skip) {
202 incidence->addAttendee(attendee);
206 // Must not have an organizer for items without attendees
207 if (!incidence->attendeeCount()) {
208 return;
211 if (mUi->mOrganizerStack->currentIndex() == 0) {
212 incidence->setOrganizer(mUi->mOrganizerCombo->currentText());
213 } else {
214 incidence->setOrganizer(mUi->mOrganizerLabel->text());
218 bool IncidenceAttendee::isDirty() const
220 if (iAmOrganizer()) {
221 KCalCore::Event tmp;
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();
228 return true;
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()) {
238 return true;
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) {
244 bool found = false;
245 for (int i = 0; i < newList.size(); ++i) {
246 if (compareAttendees(newList.at(i)->attendee(), attendee)) {
247 newList.removeAt(i);
248 found = true;
249 break;
253 if (!found) {
254 // One of the attendees in the original list was not found in the new list.
255 return true;
259 return false;
262 void IncidenceAttendee::changeStatusForMe(KCalCore::Attendee::PartStat stat)
264 const IncidenceEditorNG::EditorConfig *config = IncidenceEditorNG::EditorConfig::instance();
265 Q_ASSERT(config);
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);
277 checkDirtyStatus();
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)) {
297 uniqueList << *it;
300 mUi->mOrganizerCombo->addItems(uniqueList);
303 void IncidenceAttendee::checkIfExpansionIsNeeded(KPIM::MultiplyingLine *line)
305 AttendeeData::Ptr data = qSharedPointerDynamicCast<AttendeeData>(line->data());
306 if (!data) {
307 qDebug() << "dynamic cast failed";
308 return;
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) {
315 return;
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);
328 Q_ASSERT(expandJob);
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);
339 Q_ASSERT(searchJob);
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();
351 if (line) {
352 line->slotPropagateDeletion();
355 Akonadi::ContactGroupExpandJob *expandJob = new Akonadi::ContactGroupExpandJob(group, this);
356 connect(expandJob, &Akonadi::ContactGroupExpandJob::result, this, &IncidenceAttendee::expandResult);
357 expandJob->start();
360 void IncidenceAttendee::slotSelectAddresses()
362 #ifndef KDEPIM_MOBILE_UI
363 QWidget *dialogParent = mAttendeeEditor;
364 #else
365 QWidget *dialogParent = 0;
366 #endif
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();
374 if (dialogPtr) {
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);
383 job->start();
384 } else {
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);
395 } else {
396 qDebug() << "dialog was already deleted";
400 if (dialog.data()) {
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,
411 mParentWidget));
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);
431 checkDirtyStatus();
434 void IncidenceAttendee::slotUpdateConflictLabel(int count)
436 if (mAttendeeEditor->attendees().count() > 0) {
437 mUi->mSolveButton->setEnabled(true);
438 if (count > 0) {
439 QString label = i18ncp("@label Shows the number of scheduling conflicts",
440 "%1 conflict",
441 "%1 conflicts", count);
442 mUi->mConflictsLabel->setText(label);
443 mUi->mConflictsLabel->setVisible(true);
444 } else {
445 mUi->mConflictsLabel->setVisible(false);
447 } else {
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());
460 return true;
463 void IncidenceAttendee::insertAttendeeFromAddressee(const KContacts::Addressee &a)
465 const bool sameAsOrganizer = mUi->mOrganizerCombo &&
466 KEmailAddress::compareEmail(a.preferredEmail(),
467 mUi->mOrganizerCombo->currentText(),
468 false);
469 KCalCore::Attendee::PartStat partStat = KCalCore::Attendee::NeedsAction;
470 bool rsvp = true;
472 if (iAmOrganizer() && sameAsOrganizer) {
473 partStat = KCalCore::Attendee::Accepted;
474 rsvp = false;
476 KCalCore::Attendee::Ptr newAt(new KCalCore::Attendee(a.realName(), a.preferredEmail(),
477 rsvp,
478 partStat,
479 KCalCore::Attendee::ReqParticipant,
480 a.uid()));
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.
491 return;
494 mConflictResolver->setEarliestDateTime(start);
495 mConflictResolver->setLatestDateTime(end);
498 void IncidenceAttendee::slotOrganizerChanged(const QString &newOrganizer)
500 if (KEmailAddress::compareEmail(newOrganizer, mOrganizer, false)) {
501 return;
504 QString name;
505 QString email;
506 bool success = KEmailAddress::extractEmailAddressAndName(newOrganizer, email, name);
508 if (!success) {
509 qWarning() << "Could not extract email address and name";
510 return;
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(
529 mParentWidget,
530 i18nc("@option",
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?"));
534 } else {
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()) {
563 KCalCore::Event tmp;
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()) {
573 return;
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) {
579 bool found = false;
580 for (int i = 0; i < newList.count(); ++i) {
581 if (*newList.at(i)->attendee() == *attendee) {
582 newList.removeAt(i);
583 found = true;
584 break;
588 if (!found) {
589 qDebug() << "Attendee not found: " << attendee->email()
590 << attendee->name()
591 << attendee->status()
592 << attendee->RSVP()
593 << attendee->role()
594 << attendee->uid()
595 << attendee->delegate()
596 << attendee->delegator()
597 << "; we have:";
598 for (int i = 0; i < newList.count(); ++i) {
599 KCalCore::Attendee::Ptr attendee = newList.at(i)->attendee();
600 qDebug() << "Attendee: " << attendee->email()
601 << attendee->name()
602 << attendee->status()
603 << attendee->RSVP()
604 << attendee->role()
605 << attendee->uid()
606 << attendee->delegate()
607 << attendee->delegator();
610 return;