File indexing completed on 2024-04-28 05:11:32
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 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "editoritemmanager.h" 0009 #include "individualmailcomponentfactory.h" 0010 0011 #include <CalendarSupport/KCalPrefs> 0012 0013 #include <Akonadi/CalendarUtils> 0014 #include <Akonadi/Item> 0015 #include <Akonadi/ItemDeleteJob> 0016 #include <Akonadi/ItemFetchJob> 0017 #include <Akonadi/ItemFetchScope> 0018 #include <Akonadi/ItemMoveJob> 0019 #include <Akonadi/Monitor> 0020 #include <Akonadi/Session> 0021 #include <Akonadi/TagFetchScope> 0022 0023 #include "incidenceeditor_debug.h" 0024 #include <KJob> 0025 #include <KLocalizedString> 0026 0027 #include <QMessageBox> 0028 #include <QPointer> 0029 0030 /// ItemEditorPrivate 0031 0032 static void updateIncidenceChangerPrivacyFlags(Akonadi::IncidenceChanger *changer, IncidenceEditorNG::EditorItemManager::ItipPrivacyFlags flags) 0033 { 0034 using IncidenceEditorNG::EditorItemManager; 0035 Akonadi::IncidenceChanger::InvitationPrivacyFlags privacyFlags; 0036 privacyFlags.setFlag(Akonadi::IncidenceChanger::InvitationPrivacySign, (flags & EditorItemManager::ItipPrivacySign) == EditorItemManager::ItipPrivacySign); 0037 privacyFlags.setFlag(Akonadi::IncidenceChanger::InvitationPrivacyEncrypt, 0038 (flags & EditorItemManager::ItipPrivacyEncrypt) == EditorItemManager::ItipPrivacyEncrypt); 0039 changer->setInvitationPrivacy(privacyFlags); 0040 } 0041 0042 namespace IncidenceEditorNG 0043 { 0044 class ItemEditorPrivate 0045 { 0046 EditorItemManager *q_ptr; 0047 Q_DECLARE_PUBLIC(EditorItemManager) 0048 0049 public: 0050 Akonadi::Item mItem; 0051 Akonadi::Item mPrevItem; 0052 Akonadi::ItemFetchScope mFetchScope; 0053 Akonadi::Monitor *mItemMonitor = nullptr; 0054 ItemEditorUi *mItemUi = nullptr; 0055 bool mIsCounterProposal = false; 0056 EditorItemManager::SaveAction currentAction; 0057 Akonadi::IncidenceChanger *mChanger = nullptr; 0058 0059 public: 0060 ItemEditorPrivate(Akonadi::IncidenceChanger *changer, EditorItemManager *qq); 0061 void itemChanged(const Akonadi::Item &, const QSet<QByteArray> &); 0062 void itemFetchResult(KJob *job); 0063 void itemMoveResult(KJob *job); 0064 void onModifyFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString); 0065 0066 void onCreateFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString); 0067 0068 void setupMonitor(); 0069 void moveJobFinished(KJob *job); 0070 void setItem(const Akonadi::Item &item); 0071 }; 0072 0073 ItemEditorPrivate::ItemEditorPrivate(Akonadi::IncidenceChanger *changer, EditorItemManager *qq) 0074 : q_ptr(qq) 0075 , currentAction(EditorItemManager::None) 0076 { 0077 mFetchScope.fetchFullPayload(); 0078 mFetchScope.setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); 0079 mFetchScope.setFetchTags(true); 0080 mFetchScope.tagFetchScope().setFetchIdOnly(false); 0081 mFetchScope.setFetchRemoteIdentification(false); 0082 0083 mChanger = changer ? changer : new Akonadi::IncidenceChanger(new IndividualMailComponentFactory(qq), qq); 0084 0085 // clang-format off 0086 qq->connect(mChanger, 0087 &Akonadi::IncidenceChanger::modifyFinished, 0088 qq, 0089 [this](int, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) { 0090 onModifyFinished(item, resultCode, errorString); }); 0091 0092 qq->connect(mChanger, 0093 &Akonadi::IncidenceChanger::createFinished, 0094 qq, 0095 [this](int, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) { 0096 onCreateFinished(item, resultCode, errorString); }); 0097 // clang-format on 0098 } 0099 0100 void ItemEditorPrivate::moveJobFinished(KJob *job) 0101 { 0102 Q_Q(EditorItemManager); 0103 if (job->error()) { 0104 qCCritical(INCIDENCEEDITOR_LOG) << "Error while moving and modifying " << job->errorString(); 0105 mItemUi->reject(ItemEditorUi::ItemMoveFailed, job->errorString()); 0106 } else { 0107 Akonadi::Item item(mItem.id()); 0108 currentAction = EditorItemManager::MoveAndModify; 0109 q->load(item); 0110 } 0111 } 0112 0113 void ItemEditorPrivate::itemFetchResult(KJob *job) 0114 { 0115 Q_ASSERT(job); 0116 Q_Q(EditorItemManager); 0117 0118 EditorItemManager::SaveAction action = currentAction; 0119 currentAction = EditorItemManager::None; 0120 0121 if (job->error()) { 0122 mItemUi->reject(ItemEditorUi::ItemFetchFailed, job->errorString()); 0123 return; 0124 } 0125 0126 auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job); 0127 if (fetchJob->items().isEmpty()) { 0128 mItemUi->reject(ItemEditorUi::ItemFetchFailed); 0129 return; 0130 } 0131 0132 Akonadi::Item item = fetchJob->items().at(0); 0133 if (mItemUi->hasSupportedPayload(item)) { 0134 setItem(item); 0135 if (action != EditorItemManager::None) { 0136 // Finally enable ok/apply buttons, we've finished loading 0137 Q_EMIT q->itemSaveFinished(action); 0138 } 0139 } else { 0140 mItemUi->reject(ItemEditorUi::ItemHasInvalidPayload); 0141 } 0142 } 0143 0144 void ItemEditorPrivate::setItem(const Akonadi::Item &item) 0145 { 0146 Q_ASSERT(item.hasPayload()); 0147 mPrevItem = item; 0148 mItem = item; 0149 mItemUi->load(item); 0150 setupMonitor(); 0151 } 0152 0153 void ItemEditorPrivate::itemMoveResult(KJob *job) 0154 { 0155 Q_ASSERT(job); 0156 Q_Q(EditorItemManager); 0157 0158 if (job->error()) { 0159 auto moveJob = qobject_cast<Akonadi::ItemMoveJob *>(job); 0160 Q_ASSERT(moveJob); 0161 Q_UNUSED(moveJob) 0162 // Q_ASSERT(!moveJob->items().isEmpty()); 0163 // TODO: What is reasonable behavior at this point? 0164 qCCritical(INCIDENCEEDITOR_LOG) << "Error while moving item "; // << moveJob->items().first().id() << " to collection " 0165 //<< moveJob->destinationCollection() << job->errorString(); 0166 Q_EMIT q->itemSaveFailed(EditorItemManager::Move, job->errorString()); 0167 } else { 0168 // Fetch the item again, we want a new mItem, which has an updated parentCollection 0169 Akonadi::Item item(mItem.id()); 0170 // set currentAction, so the fetchResult slot emits itemSavedFinished(Move); 0171 // We could emit it here, but we should only enable ok/apply buttons after the loading 0172 // is complete 0173 currentAction = EditorItemManager::Move; 0174 q->load(item); 0175 } 0176 } 0177 0178 void ItemEditorPrivate::onModifyFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) 0179 { 0180 Q_Q(EditorItemManager); 0181 if (resultCode == Akonadi::IncidenceChanger::ResultCodeSuccess) { 0182 if (mItem.parentCollection() == mItemUi->selectedCollection() || mItem.storageCollectionId() == mItemUi->selectedCollection().id()) { 0183 mItem = item; 0184 Q_EMIT q->itemSaveFinished(EditorItemManager::Modify); 0185 setupMonitor(); 0186 } else { // There's a collection move too. 0187 auto moveJob = new Akonadi::ItemMoveJob(mItem, mItemUi->selectedCollection()); 0188 q->connect(moveJob, &KJob::result, q, [this](KJob *job) { 0189 moveJobFinished(job); 0190 }); 0191 } 0192 } else if (resultCode == Akonadi::IncidenceChanger::ResultCodeUserCanceled) { 0193 Q_EMIT q->itemSaveFailed(EditorItemManager::Modify, QString()); 0194 q->load(Akonadi::Item(mItem.id())); 0195 } else { 0196 qCCritical(INCIDENCEEDITOR_LOG) << "Modify failed " << errorString; 0197 Q_EMIT q->itemSaveFailed(EditorItemManager::Modify, errorString); 0198 } 0199 } 0200 0201 void ItemEditorPrivate::onCreateFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) 0202 { 0203 Q_Q(EditorItemManager); 0204 if (resultCode == Akonadi::IncidenceChanger::ResultCodeSuccess) { 0205 currentAction = EditorItemManager::Create; 0206 q->load(item); 0207 setupMonitor(); 0208 } else { 0209 qCCritical(INCIDENCEEDITOR_LOG) << "Creation failed " << errorString; 0210 Q_EMIT q->itemSaveFailed(EditorItemManager::Create, errorString); 0211 } 0212 } 0213 0214 void ItemEditorPrivate::setupMonitor() 0215 { 0216 // Q_Q(EditorItemManager); 0217 delete mItemMonitor; 0218 mItemMonitor = new Akonadi::Monitor; 0219 mItemMonitor->setObjectName(QLatin1StringView("EditorItemManagerMonitor")); 0220 mItemMonitor->ignoreSession(Akonadi::Session::defaultSession()); 0221 mItemMonitor->itemFetchScope().fetchFullPayload(); 0222 if (mItem.isValid()) { 0223 mItemMonitor->setItemMonitored(mItem); 0224 } 0225 0226 // q->connect(mItemMonitor, SIGNAL(itemChanged(Akonadi::Item,QSet<QByteArray>)), 0227 // SLOT(itemChanged(Akonadi::Item,QSet<QByteArray>))); 0228 } 0229 0230 void ItemEditorPrivate::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &partIdentifiers) 0231 { 0232 Q_Q(EditorItemManager); 0233 if (mItemUi->containsPayloadIdentifiers(partIdentifiers)) { 0234 QPointer<QMessageBox> dlg = new QMessageBox; // krazy:exclude=qclasses 0235 dlg->setIcon(QMessageBox::Question); 0236 dlg->setInformativeText( 0237 i18n("The item has been changed by another application.\n" 0238 "What should be done?")); 0239 dlg->addButton(i18n("Take over changes"), QMessageBox::AcceptRole); 0240 dlg->addButton(i18n("Ignore and Overwrite changes"), QMessageBox::RejectRole); 0241 0242 if (dlg->exec() == QMessageBox::AcceptRole) { 0243 auto job = new Akonadi::ItemFetchJob(mItem); 0244 job->setFetchScope(mFetchScope); 0245 0246 mItem = item; 0247 0248 q->load(mItem); 0249 } else { 0250 mItem.setRevision(item.revision()); 0251 q->save(); 0252 } 0253 0254 delete dlg; 0255 } 0256 0257 // Overwrite or not, we need to update the revision and the remote id to be able 0258 // to store item later on. 0259 mItem.setRevision(item.revision()); 0260 } 0261 0262 /// ItemEditor 0263 0264 EditorItemManager::EditorItemManager(ItemEditorUi *ui, Akonadi::IncidenceChanger *changer) 0265 : d_ptr(new ItemEditorPrivate(changer, this)) 0266 { 0267 Q_D(ItemEditor); 0268 d->mItemUi = ui; 0269 } 0270 0271 EditorItemManager::~EditorItemManager() = default; 0272 0273 Akonadi::Item EditorItemManager::item(ItemState state) const 0274 { 0275 Q_D(const ItemEditor); 0276 0277 switch (state) { 0278 case EditorItemManager::AfterSave: 0279 if (d->mItem.hasPayload()) { 0280 return d->mItem; 0281 } else { 0282 qCDebug(INCIDENCEEDITOR_LOG) << "Won't return mItem because isValid = " << d->mItem.isValid() << "; and haPayload is " << d->mItem.hasPayload(); 0283 } 0284 break; 0285 case EditorItemManager::BeforeSave: 0286 if (d->mPrevItem.hasPayload()) { 0287 return d->mPrevItem; 0288 } else { 0289 qCDebug(INCIDENCEEDITOR_LOG) << "Won't return mPrevItem because isValid = " << d->mPrevItem.isValid() << "; and haPayload is " 0290 << d->mPrevItem.hasPayload(); 0291 } 0292 break; 0293 } 0294 qCDebug(INCIDENCEEDITOR_LOG) << "state = " << state; 0295 Q_ASSERT_X(false, "EditorItemManager::item", "Unknown enum value"); 0296 return {}; 0297 } 0298 0299 void EditorItemManager::load(const Akonadi::Item &item) 0300 { 0301 Q_D(ItemEditor); 0302 0303 // We fetch anyways to make sure we have everything required including tags 0304 auto job = new Akonadi::ItemFetchJob(item, this); 0305 job->setFetchScope(d->mFetchScope); 0306 connect(job, &KJob::result, this, [d](KJob *job) { 0307 d->itemFetchResult(job); 0308 }); 0309 } 0310 0311 void EditorItemManager::save(ItipPrivacyFlags itipPrivacy) 0312 { 0313 Q_D(ItemEditor); 0314 0315 if (!d->mItemUi->isValid()) { 0316 Q_EMIT itemSaveFailed(d->mItem.isValid() ? Modify : Create, QString()); 0317 return; 0318 } 0319 0320 if (!d->mItemUi->isDirty() && d->mItemUi->selectedCollection() == d->mItem.parentCollection()) { 0321 // Item did not change and was not moved 0322 Q_EMIT itemSaveFinished(None); 0323 return; 0324 } 0325 0326 d->mChanger->setGroupwareCommunication(CalendarSupport::KCalPrefs::instance()->useGroupwareCommunication()); 0327 updateIncidenceChangerPrivacyFlags(d->mChanger, itipPrivacy); 0328 0329 Akonadi::Item updateItem = d->mItemUi->save(d->mItem); 0330 Q_ASSERT(updateItem.id() == d->mItem.id()); 0331 d->mItem = updateItem; 0332 0333 if (d->mItem.isValid()) { // A valid item. Means we're modifying. 0334 Q_ASSERT(d->mItem.parentCollection().isValid()); 0335 KCalendarCore::Incidence::Ptr oldPayload = Akonadi::CalendarUtils::incidence(d->mPrevItem); 0336 if (d->mItem.parentCollection() == d->mItemUi->selectedCollection() || d->mItem.storageCollectionId() == d->mItemUi->selectedCollection().id()) { 0337 (void)d->mChanger->modifyIncidence(d->mItem, oldPayload); 0338 } else { 0339 Q_ASSERT(d->mItemUi->selectedCollection().isValid()); 0340 Q_ASSERT(d->mItem.parentCollection().isValid()); 0341 0342 qCDebug(INCIDENCEEDITOR_LOG) << "Moving from" << d->mItem.parentCollection().id() << "to" << d->mItemUi->selectedCollection().id(); 0343 0344 if (d->mItemUi->isDirty()) { 0345 (void)d->mChanger->modifyIncidence(d->mItem, oldPayload); 0346 } else { 0347 auto itemMoveJob = new Akonadi::ItemMoveJob(d->mItem, d->mItemUi->selectedCollection()); 0348 connect(itemMoveJob, &KJob::result, this, [d](KJob *job) { 0349 d->itemMoveResult(job); 0350 }); 0351 } 0352 } 0353 } else { // An invalid item. Means we're creating. 0354 if (d->mIsCounterProposal) { 0355 // We don't write back to akonadi, that will be done in ITipHandler. 0356 Q_EMIT itemSaveFinished(EditorItemManager::Modify); 0357 } else { 0358 Q_ASSERT(d->mItemUi->selectedCollection().isValid()); 0359 (void)d->mChanger->createFromItem(d->mItem, d->mItemUi->selectedCollection()); 0360 } 0361 } 0362 } 0363 0364 void EditorItemManager::setIsCounterProposal(bool isCounterProposal) 0365 { 0366 Q_D(ItemEditor); 0367 d->mIsCounterProposal = isCounterProposal; 0368 } 0369 0370 ItemEditorUi::~ItemEditorUi() = default; 0371 0372 bool ItemEditorUi::isValid() const 0373 { 0374 return true; 0375 } 0376 } // namespace 0377 0378 #include "moc_editoritemmanager.cpp"