File indexing completed on 2024-04-28 05:11:35

0001 /*
0002   SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
0003   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0004   SPDX-FileCopyrightText: 2012 Allen Winter <winter@kde.org>
0005 
0006   SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "incidencedialog.h"
0010 #include "combinedincidenceeditor.h"
0011 #include "editorconfig.h"
0012 #include "incidencealarm.h"
0013 #include "incidenceattachment.h"
0014 #include "incidenceattendee.h"
0015 #include "incidencecategories.h"
0016 #include "incidencecompletionpriority.h"
0017 #include "incidencedatetime.h"
0018 #include "incidencedescription.h"
0019 #include "incidenceeditor_debug.h"
0020 #include "incidencerecurrence.h"
0021 #include "incidenceresource.h"
0022 #include "incidencesecrecy.h"
0023 #include "incidencewhatwhere.h"
0024 #include "templatemanagementdialog.h"
0025 #include "ui_dialogdesktop.h"
0026 
0027 #include "incidenceeditorsettings.h"
0028 
0029 #include <CalendarSupport/KCalPrefs>
0030 #include <CalendarSupport/Utils>
0031 
0032 #include <Akonadi/CalendarUtils>
0033 #include <Akonadi/CollectionComboBox>
0034 #include <Akonadi/ETMCalendar>
0035 #include <Akonadi/EntityTreeModel>
0036 #include <Akonadi/Item>
0037 
0038 #include <KCalUtils/Stringify>
0039 #include <KCalendarCore/ICalFormat>
0040 #include <KCalendarCore/MemoryCalendar>
0041 
0042 #include <KMessageBox>
0043 #include <KSharedConfig>
0044 
0045 #include <KWindowConfig>
0046 #include <QCloseEvent>
0047 #include <QDir>
0048 #include <QIcon>
0049 #include <QStandardPaths>
0050 #include <QTimeZone>
0051 #include <QWindow>
0052 
0053 using namespace IncidenceEditorNG;
0054 namespace
0055 {
0056 static const char myIncidenceDialogConfigGroupName[] = "IncidenceDialog";
0057 
0058 IncidenceEditorNG::EditorItemManager::ItipPrivacyFlags toItemManagerFlags(bool sign, bool encrypt)
0059 {
0060     IncidenceEditorNG::EditorItemManager::ItipPrivacyFlags flags;
0061     flags.setFlag(IncidenceEditorNG::EditorItemManager::ItipPrivacySign, sign);
0062     flags.setFlag(IncidenceEditorNG::EditorItemManager::ItipPrivacyEncrypt, encrypt);
0063     return flags;
0064 }
0065 }
0066 namespace IncidenceEditorNG
0067 {
0068 enum Tabs { GeneralTab = 0, AttendeesTab, ResourcesTab, AlarmsTab, RecurrenceTab, AttachmentsTab };
0069 
0070 class IncidenceDialogPrivate : public ItemEditorUi
0071 {
0072     IncidenceDialog *q_ptr;
0073     Q_DECLARE_PUBLIC(IncidenceDialog)
0074 
0075 public:
0076     Ui::EventOrTodoDesktop *mUi = nullptr;
0077     Akonadi::CollectionComboBox *mCalSelector = nullptr;
0078     bool mCloseOnSave = false;
0079 
0080     EditorItemManager *mItemManager = nullptr;
0081     CombinedIncidenceEditor *mEditor = nullptr;
0082     IncidenceDateTime *mIeDateTime = nullptr;
0083     IncidenceAttendee *mIeAttendee = nullptr;
0084     IncidenceRecurrence *mIeRecurrence = nullptr;
0085     IncidenceResource *mIeResource = nullptr;
0086     bool mInitiallyDirty = false;
0087     Akonadi::Item mItem;
0088     [[nodiscard]] QString typeToString(const int type) const;
0089 
0090 public:
0091     IncidenceDialogPrivate(Akonadi::IncidenceChanger *changer, IncidenceDialog *qq);
0092     ~IncidenceDialogPrivate() override;
0093 
0094     /// General methods
0095     void handleAlarmCountChange(int newCount);
0096     void handleRecurrenceChange(IncidenceEditorNG::RecurrenceType type);
0097     void loadTemplate(const QString &templateName);
0098     void manageTemplates();
0099     void saveTemplate(const QString &templateName);
0100     void storeTemplatesInConfig(const QStringList &newTemplates);
0101     void updateAttachmentCount(int newCount);
0102     void updateAttendeeCount(int newCount);
0103     void updateResourceCount(int newCount);
0104     void updateButtonStatus(bool isDirty);
0105     void showMessage(const QString &text, KMessageWidget::MessageType type);
0106     void slotInvalidCollection();
0107     void setCalendarCollection(const Akonadi::Collection &collection);
0108 
0109     /// ItemEditorUi methods
0110     [[nodiscard]] bool containsPayloadIdentifiers(const QSet<QByteArray> &partIdentifiers) const override;
0111     void handleItemSaveFinish(EditorItemManager::SaveAction);
0112     void handleItemSaveFail(EditorItemManager::SaveAction, const QString &errorMessage);
0113     [[nodiscard]] bool hasSupportedPayload(const Akonadi::Item &item) const override;
0114     [[nodiscard]] bool isDirty() const override;
0115     [[nodiscard]] bool isValid() const override;
0116     void load(const Akonadi::Item &item) override;
0117     Akonadi::Item save(const Akonadi::Item &item) override;
0118     [[nodiscard]] Akonadi::Collection selectedCollection() const override;
0119 
0120     void reject(RejectReason reason, const QString &errorMessage = QString()) override;
0121 };
0122 }
0123 
0124 IncidenceDialogPrivate::IncidenceDialogPrivate(Akonadi::IncidenceChanger *changer, IncidenceDialog *qq)
0125     : q_ptr(qq)
0126     , mUi(new Ui::EventOrTodoDesktop)
0127     , mCalSelector(new Akonadi::CollectionComboBox(changer ? changer->entityTreeModel() : nullptr))
0128     , mItemManager(new EditorItemManager(this, changer))
0129     , mEditor(new CombinedIncidenceEditor(qq))
0130 {
0131     Q_Q(IncidenceDialog);
0132     mUi->setupUi(q);
0133     mUi->mMessageWidget->hide();
0134     auto layout = new QGridLayout(mUi->mCalSelectorPlaceHolder);
0135     layout->setSpacing(0);
0136     layout->setContentsMargins(0, 0, 0, 0);
0137     layout->addWidget(mCalSelector);
0138     mCalSelector->setAccessRightsFilter(Akonadi::Collection::CanCreateItem);
0139     mUi->label->setBuddy(mCalSelector);
0140     q->connect(mCalSelector, &Akonadi::CollectionComboBox::currentChanged, q, &IncidenceDialog::handleSelectedCollectionChange);
0141 
0142     // Now instantiate the logic of the dialog. These editors update the ui, validate
0143     // fields and load/store incidences in the ui.
0144     auto ieGeneral = new IncidenceWhatWhere(mUi);
0145     mEditor->combine(ieGeneral);
0146 
0147     auto ieCategories = new IncidenceCategories(mUi);
0148     mEditor->combine(ieCategories);
0149 
0150     mIeDateTime = new IncidenceDateTime(mUi);
0151     mEditor->combine(mIeDateTime);
0152 
0153     auto ieCompletionPriority = new IncidenceCompletionPriority(mUi);
0154     mEditor->combine(ieCompletionPriority);
0155 
0156     auto ieDescription = new IncidenceDescription(mUi);
0157     mEditor->combine(ieDescription);
0158 
0159     auto ieAlarm = new IncidenceAlarm(mIeDateTime, mUi);
0160     mEditor->combine(ieAlarm);
0161 
0162     auto ieAttachments = new IncidenceAttachment(mUi);
0163     mEditor->combine(ieAttachments);
0164 
0165     mIeRecurrence = new IncidenceRecurrence(mIeDateTime, mUi);
0166     mEditor->combine(mIeRecurrence);
0167 
0168     auto ieSecrecy = new IncidenceSecrecy(mUi);
0169     mEditor->combine(ieSecrecy);
0170 
0171     mIeAttendee = new IncidenceAttendee(qq, mIeDateTime, mUi);
0172     mIeAttendee->setParent(qq);
0173     mEditor->combine(mIeAttendee);
0174 
0175     mIeResource = new IncidenceResource(mIeAttendee, mIeDateTime, mUi);
0176     mEditor->combine(mIeResource);
0177 
0178     // Set the default collection
0179     const qint64 colId = CalendarSupport::KCalPrefs::instance()->defaultCalendarId();
0180     const Akonadi::Collection col(colId);
0181     setCalendarCollection(col);
0182 
0183     q->connect(mEditor, &CombinedIncidenceEditor::showMessage, q, [this](const QString &reason, KMessageWidget::MessageType msgType) {
0184         showMessage(reason, msgType);
0185     });
0186     q->connect(mEditor, &IncidenceEditor::dirtyStatusChanged, q, [this](bool isDirty) {
0187         updateButtonStatus(isDirty);
0188     });
0189     q->connect(mItemManager, &EditorItemManager::itemSaveFinished, q, [this](EditorItemManager::SaveAction action) {
0190         handleItemSaveFinish(action);
0191     });
0192     q->connect(mItemManager, &EditorItemManager::itemSaveFailed, q, [this](EditorItemManager::SaveAction action, const QString &message) {
0193         handleItemSaveFail(action, message);
0194     });
0195     q->connect(ieAlarm, &IncidenceAlarm::alarmCountChanged, q, [this](int newCount) {
0196         handleAlarmCountChange(newCount);
0197     });
0198     q->connect(mIeRecurrence, &IncidenceRecurrence::recurrenceChanged, q, [this](IncidenceEditorNG::RecurrenceType type) {
0199         handleRecurrenceChange(type);
0200     });
0201     q->connect(ieAttachments, &IncidenceAttachment::attachmentCountChanged, q, [this](int newCount) {
0202         updateAttachmentCount(newCount);
0203     });
0204     q->connect(mIeAttendee, &IncidenceAttendee::attendeeCountChanged, q, [this](int count) {
0205         updateAttendeeCount(count);
0206     });
0207     q->connect(mIeResource, &IncidenceResource::resourceCountChanged, q, [this](int count) {
0208         updateResourceCount(count);
0209     });
0210 }
0211 
0212 IncidenceDialogPrivate::~IncidenceDialogPrivate()
0213 {
0214     delete mItemManager;
0215     delete mEditor;
0216     delete mUi;
0217 }
0218 
0219 void IncidenceDialogPrivate::slotInvalidCollection()
0220 {
0221     showMessage(i18n("Select a valid collection first."), KMessageWidget::Warning);
0222 }
0223 
0224 void IncidenceDialogPrivate::setCalendarCollection(const Akonadi::Collection &collection)
0225 {
0226     if (collection.isValid()) {
0227         mCalSelector->setDefaultCollection(collection);
0228     } else {
0229         mCalSelector->setCurrentIndex(0);
0230     }
0231 }
0232 
0233 void IncidenceDialogPrivate::showMessage(const QString &text, KMessageWidget::MessageType type)
0234 {
0235     mUi->mMessageWidget->setText(text);
0236     mUi->mMessageWidget->setMessageType(type);
0237     mUi->mMessageWidget->show();
0238 }
0239 
0240 void IncidenceDialogPrivate::handleAlarmCountChange(int newCount)
0241 {
0242     QString tabText;
0243     if (newCount > 0) {
0244         tabText = i18nc("@title:tab Tab to configure the reminders of an event or todo", "Reminder (%1)", newCount);
0245     } else {
0246         tabText = i18nc("@title:tab Tab to configure the reminders of an event or todo", "Reminder");
0247     }
0248 
0249     mUi->mTabWidget->setTabText(AlarmsTab, tabText);
0250 }
0251 
0252 void IncidenceDialogPrivate::handleRecurrenceChange(IncidenceEditorNG::RecurrenceType type)
0253 {
0254     QString tabText = i18nc("@title:tab Tab to configure the recurrence of an event or todo", "Rec&urrence");
0255 
0256     // Keep this numbers in sync with the items in mUi->mRecurrenceTypeCombo. I
0257     // tried adding an enum to IncidenceRecurrence but for whatever reason I could
0258     // Qt not play nice with namespaced enums in signal/slot connections.
0259     // Anyways, I don't expect these values to change.
0260     switch (type) {
0261     case RecurrenceTypeNone:
0262         break;
0263     case RecurrenceTypeDaily:
0264         tabText += i18nc("@title:tab Daily recurring event, capital first letter only", " (D)");
0265         break;
0266     case RecurrenceTypeWeekly:
0267         tabText += i18nc("@title:tab Weekly recurring event, capital first letter only", " (W)");
0268         break;
0269     case RecurrenceTypeMonthly:
0270         tabText += i18nc("@title:tab Monthly recurring event, capital first letter only", " (M)");
0271         break;
0272     case RecurrenceTypeYearly:
0273         tabText += i18nc("@title:tab Yearly recurring event, capital first letter only", " (Y)");
0274         break;
0275     case RecurrenceTypeException:
0276         tabText += i18nc("@title:tab Exception to a recurring event, capital first letter only", " (E)");
0277         break;
0278     default:
0279         Q_ASSERT_X(false, "handleRecurrenceChange", "Fix your program");
0280     }
0281 
0282     mUi->mTabWidget->setTabText(RecurrenceTab, tabText);
0283 }
0284 
0285 QString IncidenceDialogPrivate::typeToString(const int type) const
0286 {
0287     // Do not translate.
0288     switch (type) {
0289     case KCalendarCore::Incidence::TypeEvent:
0290         return QStringLiteral("Event");
0291     case KCalendarCore::Incidence::TypeTodo:
0292         return QStringLiteral("Todo");
0293     case KCalendarCore::Incidence::TypeJournal:
0294         return QStringLiteral("Journal");
0295     default:
0296         return QStringLiteral("Unknown");
0297     }
0298 }
0299 
0300 void IncidenceDialogPrivate::loadTemplate(const QString &templateName)
0301 {
0302     Q_Q(IncidenceDialog);
0303 
0304     KCalendarCore::MemoryCalendar::Ptr cal(new KCalendarCore::MemoryCalendar(QTimeZone::systemTimeZone()));
0305 
0306     const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0307                                                     QStringLiteral("/korganizer/templates/") + typeToString(mEditor->type()) + QLatin1Char('/') + templateName);
0308 
0309     if (fileName.isEmpty()) {
0310         KMessageBox::error(q, i18nc("@info", "Unable to find template '%1'.", templateName));
0311         return;
0312     }
0313 
0314     KCalendarCore::ICalFormat format;
0315     if (!format.load(cal, fileName)) {
0316         KMessageBox::error(q, i18nc("@info", "Error loading template file '%1'.", fileName));
0317         return;
0318     }
0319 
0320     KCalendarCore::Incidence::List incidences = cal->incidences();
0321     if (incidences.isEmpty()) {
0322         KMessageBox::error(q, i18nc("@info", "Template does not contain a valid incidence."));
0323         return;
0324     }
0325 
0326     mIeDateTime->setActiveDate(QDate());
0327     KCalendarCore::Incidence::Ptr newInc = KCalendarCore::Incidence::Ptr(incidences.first()->clone());
0328     newInc->setUid(KCalendarCore::CalFormat::createUniqueId());
0329 
0330     // We add a custom property so that some fields aren't loaded, dates for example
0331     newInc->setCustomProperty(QByteArray("kdepim"), "isTemplate", QStringLiteral("true"));
0332     mEditor->load(newInc);
0333     newInc->removeCustomProperty(QByteArray(), "isTemplate");
0334 }
0335 
0336 void IncidenceDialogPrivate::manageTemplates()
0337 {
0338     Q_Q(IncidenceDialog);
0339 
0340     QStringList &templates = IncidenceEditorNG::EditorConfig::instance()->templates(mEditor->type());
0341 
0342     QPointer<IncidenceEditorNG::TemplateManagementDialog> dialog(
0343         new IncidenceEditorNG::TemplateManagementDialog(q, templates, KCalUtils::Stringify::incidenceType(mEditor->type())));
0344 
0345     q->connect(dialog, &TemplateManagementDialog::loadTemplate, q, [this](const QString &templateName) {
0346         loadTemplate(templateName);
0347     });
0348     q->connect(dialog, &TemplateManagementDialog::templatesChanged, q, [this](const QStringList &templates) {
0349         storeTemplatesInConfig(templates);
0350     });
0351     q->connect(dialog, &TemplateManagementDialog::saveTemplate, q, [this](const QString &templateName) {
0352         saveTemplate(templateName);
0353     });
0354     dialog->exec();
0355     delete dialog;
0356 }
0357 
0358 void IncidenceDialogPrivate::saveTemplate(const QString &templateName)
0359 {
0360     Q_ASSERT(!templateName.isEmpty());
0361 
0362     KCalendarCore::MemoryCalendar::Ptr cal(new KCalendarCore::MemoryCalendar(QTimeZone::systemTimeZone()));
0363 
0364     switch (mEditor->type()) {
0365     case KCalendarCore::Incidence::TypeEvent: {
0366         KCalendarCore::Event::Ptr event(new KCalendarCore::Event());
0367         mEditor->save(event);
0368         cal->addEvent(KCalendarCore::Event::Ptr(event->clone()));
0369         break;
0370     }
0371     case KCalendarCore::Incidence::TypeTodo: {
0372         KCalendarCore::Todo::Ptr todo(new KCalendarCore::Todo);
0373         mEditor->save(todo);
0374         cal->addTodo(KCalendarCore::Todo::Ptr(todo->clone()));
0375         break;
0376     }
0377     case KCalendarCore::Incidence::TypeJournal: {
0378         KCalendarCore::Journal::Ptr journal(new KCalendarCore::Journal);
0379         mEditor->save(journal);
0380         cal->addJournal(KCalendarCore::Journal::Ptr(journal->clone()));
0381         break;
0382     }
0383     default:
0384         Q_ASSERT_X(false, "saveTemplate", "Fix your program");
0385     }
0386 
0387     QString fileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/templates/")
0388         + typeToString(mEditor->type()) + QLatin1Char('/');
0389     QDir().mkpath(fileName);
0390     fileName += templateName;
0391 
0392     KCalendarCore::ICalFormat format;
0393     format.save(cal, fileName);
0394 }
0395 
0396 void IncidenceDialogPrivate::storeTemplatesInConfig(const QStringList &templateNames)
0397 {
0398     // I find this somewhat broken. templates() returns a reference, maybe it should
0399     // be changed by adding a setTemplates method.
0400     const QStringList origTemplates = IncidenceEditorNG::EditorConfig::instance()->templates(mEditor->type());
0401     const QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("korganizer/templates/")
0402         + typeToString(mEditor->type()) + QLatin1Char('/');
0403     QDir().mkpath(defaultPath);
0404     for (const QString &tmpl : origTemplates) {
0405         if (!templateNames.contains(tmpl)) {
0406             const QString fileName = defaultPath + tmpl;
0407             QFile file(fileName);
0408             if (file.exists()) {
0409                 file.remove();
0410             }
0411         }
0412     }
0413 
0414     IncidenceEditorNG::EditorConfig::instance()->templates(mEditor->type()) = templateNames;
0415     IncidenceEditorNG::EditorConfig::instance()->config()->save();
0416 }
0417 
0418 void IncidenceDialogPrivate::updateAttachmentCount(int newCount)
0419 {
0420     if (newCount > 0) {
0421         mUi->mTabWidget->setTabText(AttachmentsTab, i18nc("@title:tab Tab to modify attachments of an event or todo", "Attac&hments (%1)", newCount));
0422     } else {
0423         mUi->mTabWidget->setTabText(AttachmentsTab, i18nc("@title:tab Tab to modify attachments of an event or todo", "Attac&hments"));
0424     }
0425 }
0426 
0427 void IncidenceDialogPrivate::updateAttendeeCount(int newCount)
0428 {
0429     if (newCount > 0) {
0430         mUi->mTabWidget->setTabText(AttendeesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Attendees (%1)", newCount));
0431     } else {
0432         mUi->mTabWidget->setTabText(AttendeesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Attendees"));
0433     }
0434 }
0435 
0436 void IncidenceDialogPrivate::updateResourceCount(int newCount)
0437 {
0438     if (newCount > 0) {
0439         mUi->mTabWidget->setTabText(ResourcesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Resources (%1)", newCount));
0440     } else {
0441         mUi->mTabWidget->setTabText(ResourcesTab, i18nc("@title:tab Tab to modify attendees of an event or todo", "&Resources"));
0442     }
0443 }
0444 
0445 void IncidenceDialogPrivate::updateButtonStatus(bool isDirty)
0446 {
0447     mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(isDirty || mInitiallyDirty);
0448 }
0449 
0450 bool IncidenceDialogPrivate::containsPayloadIdentifiers(const QSet<QByteArray> &partIdentifiers) const
0451 {
0452     return partIdentifiers.contains(QByteArray("PLD:RFC822"));
0453 }
0454 
0455 void IncidenceDialogPrivate::handleItemSaveFail(EditorItemManager::SaveAction, const QString &errorMessage)
0456 {
0457     Q_Q(IncidenceDialog);
0458 
0459     bool retry = false;
0460 
0461     if (!errorMessage.isEmpty()) {
0462         const QString message = i18nc("@info",
0463                                       "Unable to store the incidence in the calendar. Try again?\n\n "
0464                                       "Reason: %1",
0465                                       errorMessage);
0466         const int answer = KMessageBox::warningTwoActions(q,
0467                                                           message,
0468                                                           QString(),
0469                                                           KGuiItem(i18nc("@action:button", "Retry"), QStringLiteral("dialog-ok")),
0470                                                           KStandardGuiItem::cancel());
0471         retry = (answer == KMessageBox::ButtonCode::PrimaryAction);
0472     }
0473 
0474     if (retry) {
0475         mItemManager->save();
0476     } else {
0477         updateButtonStatus(isDirty());
0478         mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
0479         mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
0480     }
0481 }
0482 
0483 void IncidenceDialogPrivate::handleItemSaveFinish(EditorItemManager::SaveAction saveAction)
0484 {
0485     Q_Q(IncidenceDialog);
0486 
0487     if ((mEditor->type() == KCalendarCore::Incidence::TypeEvent) && (mCalSelector->count() > 1)
0488         && (CalendarSupport::KCalPrefs::instance()->defaultCalendarId() == -1)) {
0489         const QString collectionName = mCalSelector->currentText();
0490         const QString message = xi18nc("@info",
0491                                        "<para>You have not set a default calendar for your events yet.</para>"
0492                                        "<para>Setting a default calendar will make creating new events faster and "
0493                                        "easier with less chance of filing them into the wrong folder.</para>"
0494                                        "<para>Would you like to set your default events calendar to "
0495                                        "<resource>%1</resource>?</para>",
0496                                        collectionName);
0497         const int answer = KMessageBox::questionTwoActions(q,
0498                                                            message,
0499                                                            i18nc("@title:window", "Set Default Calendar?"),
0500                                                            KGuiItem(i18nc("@action:button", "Set As Default"), QStringLiteral("dialog-ok")),
0501                                                            KGuiItem(i18nc("@action:button", "Do Not Set"), QStringLiteral("dialog-cancel")),
0502                                                            QStringLiteral("setDefaultCalendarCollection"));
0503         if (answer == KMessageBox::ButtonCode::PrimaryAction) {
0504             CalendarSupport::KCalPrefs::instance()->setDefaultCalendarId(mItem.storageCollectionId());
0505         }
0506     }
0507 
0508     if (mCloseOnSave) {
0509         q->accept();
0510     } else {
0511         const Akonadi::Item item = mItemManager->item();
0512         Q_ASSERT(item.isValid());
0513         Q_ASSERT(item.hasPayload());
0514         Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
0515         // Now the item is successfully saved, reload it in the editor in order to
0516         // reset the dirty status of the editor.
0517         mEditor->load(item.payload<KCalendarCore::Incidence::Ptr>());
0518         mEditor->load(item);
0519 
0520         // Set the buttons to a reasonable state as well (ok and apply should be
0521         // disabled at this point).
0522         mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
0523         mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
0524         mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(isDirty());
0525     }
0526 
0527     if (saveAction == EditorItemManager::Create) {
0528         Q_EMIT q->incidenceCreated(mItemManager->item());
0529     }
0530 }
0531 
0532 bool IncidenceDialogPrivate::hasSupportedPayload(const Akonadi::Item &item) const
0533 {
0534     return !Akonadi::CalendarUtils::incidence(item).isNull();
0535 }
0536 
0537 bool IncidenceDialogPrivate::isDirty() const
0538 {
0539     if (mItem.isValid()) {
0540         return mEditor->isDirty() || mCalSelector->currentCollection().id() != mItem.storageCollectionId();
0541     } else {
0542         return mEditor->isDirty();
0543     }
0544 }
0545 
0546 bool IncidenceDialogPrivate::isValid() const
0547 {
0548     Q_Q(const IncidenceDialog);
0549     if (mEditor->isValid()) {
0550         // Check if there's a selected collection.
0551         if (mCalSelector->currentCollection().isValid()) {
0552             return true;
0553         } else {
0554             qCWarning(INCIDENCEEDITOR_LOG) << "Select a collection first";
0555             Q_EMIT q->invalidCollection();
0556         }
0557     }
0558 
0559     return false;
0560 }
0561 
0562 void IncidenceDialogPrivate::load(const Akonadi::Item &item)
0563 {
0564     Q_Q(IncidenceDialog);
0565 
0566     Q_ASSERT(hasSupportedPayload(item));
0567 
0568     if (CalendarSupport::hasJournal(item)) {
0569         // mUi->mTabWidget->removeTab(5);
0570         mUi->mTabWidget->removeTab(AttachmentsTab);
0571         mUi->mTabWidget->removeTab(RecurrenceTab);
0572         mUi->mTabWidget->removeTab(AlarmsTab);
0573         mUi->mTabWidget->removeTab(AttendeesTab);
0574         mUi->mTabWidget->removeTab(ResourcesTab);
0575     }
0576 
0577     mEditor->load(Akonadi::CalendarUtils::incidence(item));
0578     mEditor->load(item);
0579 
0580     const KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0581     const QStringList allEmails = IncidenceEditorNG::EditorConfig::instance()->allEmails();
0582     const KCalendarCore::Attendee me = incidence->attendeeByMails(allEmails);
0583 
0584     if (incidence->attendeeCount() > 1 // >1 because you won't drink alone
0585         && !me.isNull()
0586         && (me.status() == KCalendarCore::Attendee::NeedsAction || me.status() == KCalendarCore::Attendee::Tentative
0587             || me.status() == KCalendarCore::Attendee::InProcess)) {
0588         // Show the invitation bar: "You are invited [accept] [decline]"
0589         mUi->mInvitationBar->show();
0590     } else {
0591         mUi->mInvitationBar->hide();
0592     }
0593 
0594     qCDebug(INCIDENCEEDITOR_LOG) << "Loading item " << item.id() << "; parent " << item.parentCollection().id() << "; storage " << item.storageCollectionId();
0595 
0596     if (item.storageCollectionId() > -1) {
0597         mCalSelector->setDefaultCollection(Akonadi::Collection(item.storageCollectionId()));
0598     }
0599 
0600     if (!mCalSelector->mimeTypeFilter().contains(incidence->mimeType())) {
0601         mCalSelector->setMimeTypeFilter({incidence->mimeType()});
0602     }
0603 
0604     if (mEditor->type() == KCalendarCore::Incidence::TypeTodo) {
0605         q->setWindowIcon(QIcon::fromTheme(QStringLiteral("view-calendar-tasks")));
0606     } else if (mEditor->type() == KCalendarCore::Incidence::TypeEvent) {
0607         q->setWindowIcon(QIcon::fromTheme(QStringLiteral("view-calendar-day")));
0608     } else if (mEditor->type() == KCalendarCore::Incidence::TypeJournal) {
0609         q->setWindowIcon(QIcon::fromTheme(QStringLiteral("view-pim-journal")));
0610     }
0611 
0612     // Initialize tab's titles
0613     updateAttachmentCount(incidence->attachments().size());
0614     updateResourceCount(mIeResource->resourceCount());
0615     updateAttendeeCount(mIeAttendee->attendeeCount());
0616     handleRecurrenceChange(mIeRecurrence->currentRecurrenceType());
0617     handleAlarmCountChange(incidence->alarms().count());
0618 
0619     mItem = item;
0620 
0621     q->show();
0622 }
0623 
0624 Akonadi::Item IncidenceDialogPrivate::save(const Akonadi::Item &item)
0625 {
0626     Q_ASSERT(mEditor->incidence<KCalendarCore::Incidence>());
0627 
0628     KCalendarCore::Incidence::Ptr incidenceInEditor = mEditor->incidence<KCalendarCore::Incidence>();
0629     KCalendarCore::Incidence::Ptr newIncidence(incidenceInEditor->clone());
0630 
0631     Akonadi::Item result = item;
0632     result.setMimeType(newIncidence->mimeType());
0633 
0634     // There's no editor that has the relatedTo property. We must set it here, by hand.
0635     // Otherwise it gets lost.
0636     // FIXME: Why don't we clone() incidenceInEditor then pass the clone to save(),
0637     // I wonder if we're not leaking other properties.
0638     newIncidence->setRelatedTo(incidenceInEditor->relatedTo());
0639 
0640     mEditor->save(newIncidence);
0641     mEditor->save(result);
0642 
0643     // Make sure that we don't loose uid for existing incidence
0644     newIncidence->setUid(mEditor->incidence<KCalendarCore::Incidence>()->uid());
0645 
0646     // Mark the incidence as changed
0647     if (mItem.isValid()) {
0648         newIncidence->setRevision(newIncidence->revision() + 1);
0649     }
0650 
0651     result.setPayload<KCalendarCore::Incidence::Ptr>(newIncidence);
0652     return result;
0653 }
0654 
0655 Akonadi::Collection IncidenceDialogPrivate::selectedCollection() const
0656 {
0657     return mCalSelector->currentCollection();
0658 }
0659 
0660 void IncidenceDialogPrivate::reject(RejectReason reason, const QString &errorMessage)
0661 {
0662     Q_UNUSED(reason)
0663 
0664     Q_Q(IncidenceDialog);
0665     qCCritical(INCIDENCEEDITOR_LOG) << "Rejecting:" << errorMessage;
0666     q->deleteLater();
0667 }
0668 
0669 /// IncidenceDialog
0670 
0671 IncidenceDialog::IncidenceDialog(Akonadi::IncidenceChanger *changer, QWidget *parent, Qt::WindowFlags flags)
0672     : QDialog(parent, flags)
0673     , d_ptr(new IncidenceDialogPrivate(changer, this))
0674 {
0675     Q_D(IncidenceDialog);
0676     setAttribute(Qt::WA_DeleteOnClose);
0677 
0678     d->mUi->mTabWidget->setCurrentIndex(0);
0679     d->mUi->mSummaryEdit->setFocus();
0680 
0681     d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setToolTip(i18nc("@info:tooltip", "Save current changes"));
0682     d->mUi->buttonBox->button(QDialogButtonBox::Ok)->setToolTip(i18nc("@action:button", "Save changes and close dialog"));
0683     d->mUi->buttonBox->button(QDialogButtonBox::Cancel)->setToolTip(i18nc("@action:button", "Discard changes and close dialog"));
0684     d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0685 
0686     auto defaultButton = d->mUi->buttonBox->button(QDialogButtonBox::RestoreDefaults);
0687     defaultButton->setText(i18nc("@action:button", "&Templates..."));
0688     defaultButton->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template")));
0689     defaultButton->setToolTip(i18nc("@info:tooltip", "Manage templates for this item"));
0690     defaultButton->setWhatsThis(i18nc("@info:whatsthis",
0691                                       "Push this button to show a dialog that helps "
0692                                       "you manage a set of templates. Templates "
0693                                       "can make creating new items easier and faster "
0694                                       "by putting your favorite default values into "
0695                                       "the editor automatically."));
0696 
0697     connect(d->mUi->buttonBox, &QDialogButtonBox::clicked, this, &IncidenceDialog::slotButtonClicked);
0698 
0699     setModal(false);
0700 
0701     connect(d->mUi->mAcceptInvitationButton, &QAbstractButton::clicked, d->mIeAttendee, &IncidenceAttendee::acceptForMe);
0702     connect(d->mUi->mAcceptInvitationButton, &QAbstractButton::clicked, d->mUi->mInvitationBar, &QWidget::hide);
0703     connect(d->mUi->mDeclineInvitationButton, &QAbstractButton::clicked, d->mIeAttendee, &IncidenceAttendee::declineForMe);
0704     connect(d->mUi->mDeclineInvitationButton, &QAbstractButton::clicked, d->mUi->mInvitationBar, &QWidget::hide);
0705     connect(this, &IncidenceDialog::invalidCollection, this, [d]() {
0706         d->slotInvalidCollection();
0707     });
0708     readConfig();
0709 }
0710 
0711 IncidenceDialog::~IncidenceDialog()
0712 {
0713     writeConfig();
0714 }
0715 
0716 void IncidenceDialog::writeConfig()
0717 {
0718     KConfigGroup group(KSharedConfig::openStateConfig(), QLatin1StringView(myIncidenceDialogConfigGroupName));
0719     KWindowConfig::saveWindowSize(windowHandle(), group);
0720 }
0721 
0722 void IncidenceDialog::readConfig()
0723 {
0724     create(); // ensure a window is created
0725     windowHandle()->resize(QSize(500, 500));
0726     KConfigGroup group(KSharedConfig::openStateConfig(), QLatin1StringView(myIncidenceDialogConfigGroupName));
0727     KWindowConfig::restoreWindowSize(windowHandle(), group);
0728     resize(windowHandle()->size()); // workaround for QTBUG-40584
0729 }
0730 
0731 void IncidenceDialog::load(const Akonadi::Item &item, const QDate &activeDate)
0732 {
0733     Q_D(IncidenceDialog);
0734     d->mIeDateTime->setActiveDate(activeDate);
0735     if (item.isValid()) { // We're editing
0736         d->mItemManager->load(item);
0737     } else { // We're creating
0738         Q_ASSERT(d->hasSupportedPayload(item));
0739         d->load(item);
0740         show();
0741     }
0742 }
0743 
0744 void IncidenceDialog::selectCollection(const Akonadi::Collection &collection)
0745 {
0746     Q_D(IncidenceDialog);
0747     d->setCalendarCollection(collection);
0748 }
0749 
0750 void IncidenceDialog::setIsCounterProposal(bool isCounterProposal)
0751 {
0752     Q_D(IncidenceDialog);
0753     d->mItemManager->setIsCounterProposal(isCounterProposal);
0754 }
0755 
0756 QObject *IncidenceDialog::typeAheadReceiver() const
0757 {
0758     Q_D(const IncidenceDialog);
0759     return d->mUi->mSummaryEdit;
0760 }
0761 
0762 void IncidenceDialog::slotButtonClicked(QAbstractButton *button)
0763 {
0764     Q_D(IncidenceDialog);
0765 
0766     if (d->mUi->buttonBox->button(QDialogButtonBox::Ok) == button) {
0767         if (d->isDirty() || d->mInitiallyDirty) {
0768             d->mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0769             d->mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
0770             d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0771             d->mCloseOnSave = true;
0772             d->mInitiallyDirty = false;
0773             d->mItemManager->save(toItemManagerFlags(d->mUi->mSignItip->isChecked(), d->mUi->mEncryptItip->isChecked()));
0774         } else {
0775             close();
0776         }
0777     } else if (d->mUi->buttonBox->button(QDialogButtonBox::Apply) == button) {
0778         d->mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0779         d->mUi->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
0780         d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0781 
0782         d->mCloseOnSave = false;
0783         d->mInitiallyDirty = false;
0784         d->mItemManager->save(toItemManagerFlags(d->mUi->mSignItip->isChecked(), d->mUi->mEncryptItip->isChecked()));
0785     } else if (d->mUi->buttonBox->button(QDialogButtonBox::Cancel) == button) {
0786         if (d->isDirty()
0787             && KMessageBox::questionTwoActions(this,
0788                                                i18nc("@info", "Do you really want to cancel?"),
0789                                                i18nc("@title:window", "KOrganizer Confirmation"),
0790                                                KGuiItem(i18nc("@action:button", "Cancel Editing"), QStringLiteral("dialog-ok")),
0791                                                KGuiItem(i18nc("@action:button", "Do Not Cancel"), QStringLiteral("dialog-cancel")))
0792                 == KMessageBox::ButtonCode::PrimaryAction) {
0793             QDialog::reject(); // Discard current changes
0794         } else if (!d->isDirty()) {
0795             QDialog::reject(); // No pending changes, just close the dialog.
0796         } // else { // the user wasn't finished editing after all }
0797     } else if (d->mUi->buttonBox->button(QDialogButtonBox::RestoreDefaults)) {
0798         d->manageTemplates();
0799     } else {
0800         Q_ASSERT(false); // Shouldn't happen
0801     }
0802 }
0803 
0804 void IncidenceDialog::reject()
0805 {
0806     Q_D(IncidenceDialog);
0807     if (d->isDirty()
0808         && KMessageBox::questionTwoActions(this,
0809                                            i18nc("@info", "Do you really want to cancel?"),
0810                                            i18nc("@title:window", "KOrganizer Confirmation"),
0811                                            KGuiItem(i18nc("@action:button", "Cancel Editing"), QStringLiteral("dialog-ok")),
0812                                            KGuiItem(i18nc("@action:button", "Do Not Cancel"), QStringLiteral("dialog-cancel")))
0813             == KMessageBox::ButtonCode::PrimaryAction) {
0814         QDialog::reject(); // Discard current changes
0815     } else if (!d->isDirty()) {
0816         QDialog::reject(); // No pending changes, just close the dialog.
0817     }
0818 }
0819 
0820 void IncidenceDialog::closeEvent(QCloseEvent *event)
0821 {
0822     Q_D(IncidenceDialog);
0823     if (d->isDirty()
0824         && KMessageBox::questionTwoActions(this,
0825                                            i18nc("@info", "Do you really want to cancel?"),
0826                                            i18nc("@title:window", "KOrganizer Confirmation"),
0827                                            KGuiItem(i18nc("@action:button", "Cancel Editing"), QStringLiteral("dialog-ok")),
0828                                            KGuiItem(i18nc("@action:button", "Do Not Cancel"), QStringLiteral("dialog-cancel")))
0829             == KMessageBox::ButtonCode::PrimaryAction) {
0830         QDialog::reject(); // Discard current changes
0831         QDialog::closeEvent(event);
0832     } else if (!d->isDirty()) {
0833         QDialog::reject(); // No pending changes, just close the dialog.
0834         QDialog::closeEvent(event);
0835     } else {
0836         event->ignore();
0837     }
0838 }
0839 
0840 void IncidenceDialog::setInitiallyDirty(bool initiallyDirty)
0841 {
0842     Q_D(IncidenceDialog);
0843     d->mInitiallyDirty = initiallyDirty;
0844 }
0845 
0846 Akonadi::Item IncidenceDialog::item() const
0847 {
0848     Q_D(const IncidenceDialog);
0849     return d->mItemManager->item();
0850 }
0851 
0852 void IncidenceDialog::handleSelectedCollectionChange(const Akonadi::Collection &collection)
0853 {
0854     Q_D(IncidenceDialog);
0855     if (d->mItem.parentCollection().isValid()) {
0856         d->mUi->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(collection.id() != d->mItem.parentCollection().id());
0857     }
0858 }
0859 
0860 #include "moc_incidencedialog.cpp"