Incidenceeditor

editoritemmanager.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
3 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "editoritemmanager.h"
9#include "individualmailcomponentfactory.h"
10
11#include <CalendarSupport/KCalPrefs>
12
13#include <Akonadi/CalendarUtils>
14#include <Akonadi/Item>
15#include <Akonadi/ItemDeleteJob>
16#include <Akonadi/ItemFetchJob>
17#include <Akonadi/ItemFetchScope>
18#include <Akonadi/ItemMoveJob>
19#include <Akonadi/Monitor>
20#include <Akonadi/Session>
21#include <Akonadi/TagFetchScope>
22
23#include "incidenceeditor_debug.h"
24#include <KJob>
25#include <KLocalizedString>
26
27#include <QMessageBox>
28#include <QPointer>
29
30/// ItemEditorPrivate
31
32static void updateIncidenceChangerPrivacyFlags(Akonadi::IncidenceChanger *changer, IncidenceEditorNG::EditorItemManager::ItipPrivacyFlags flags)
33{
35 Akonadi::IncidenceChanger::InvitationPrivacyFlags privacyFlags;
36 privacyFlags.setFlag(Akonadi::IncidenceChanger::InvitationPrivacySign, (flags & EditorItemManager::ItipPrivacySign) == EditorItemManager::ItipPrivacySign);
37 privacyFlags.setFlag(Akonadi::IncidenceChanger::InvitationPrivacyEncrypt,
38 (flags & EditorItemManager::ItipPrivacyEncrypt) == EditorItemManager::ItipPrivacyEncrypt);
39 changer->setInvitationPrivacy(privacyFlags);
40}
41
42namespace IncidenceEditorNG
43{
44class ItemEditorPrivate
45{
46 EditorItemManager *q_ptr;
47 Q_DECLARE_PUBLIC(EditorItemManager)
48
49public:
50 Akonadi::Item mItem;
51 Akonadi::Item mPrevItem;
52 Akonadi::ItemFetchScope mFetchScope;
53 Akonadi::Monitor *mItemMonitor = nullptr;
54 ItemEditorUi *mItemUi = nullptr;
55 bool mIsCounterProposal = false;
57 Akonadi::IncidenceChanger *mChanger = nullptr;
58
59public:
60 ItemEditorPrivate(Akonadi::IncidenceChanger *changer, EditorItemManager *qq);
61 void itemChanged(const Akonadi::Item &, const QSet<QByteArray> &);
62 void itemFetchResult(KJob *job);
63 void itemMoveResult(KJob *job);
64 void onModifyFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString);
65
66 void onCreateFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString);
67
68 void setupMonitor();
69 void moveJobFinished(KJob *job);
70 void setItem(const Akonadi::Item &item);
71};
72
73ItemEditorPrivate::ItemEditorPrivate(Akonadi::IncidenceChanger *changer, EditorItemManager *qq)
74 : q_ptr(qq)
75 , currentAction(EditorItemManager::None)
76{
77 mFetchScope.fetchFullPayload();
78 mFetchScope.setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);
79 mFetchScope.setFetchTags(true);
80 mFetchScope.tagFetchScope().setFetchIdOnly(false);
81 mFetchScope.setFetchRemoteIdentification(false);
82
83 mChanger = changer ? changer : new Akonadi::IncidenceChanger(new IndividualMailComponentFactory(qq), qq);
84
85 // clang-format off
86 qq->connect(mChanger,
87 &Akonadi::IncidenceChanger::modifyFinished,
88 qq,
89 [this](int, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) {
90 onModifyFinished(item, resultCode, errorString); });
91
92 qq->connect(mChanger,
93 &Akonadi::IncidenceChanger::createFinished,
94 qq,
95 [this](int, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) {
96 onCreateFinished(item, resultCode, errorString); });
97 // clang-format on
98}
99
100void ItemEditorPrivate::moveJobFinished(KJob *job)
101{
102 Q_Q(EditorItemManager);
103 if (job->error()) {
104 qCCritical(INCIDENCEEDITOR_LOG) << "Error while moving and modifying " << job->errorString();
105 mItemUi->reject(ItemEditorUi::ItemMoveFailed, job->errorString());
106 } else {
107 Akonadi::Item item(mItem.id());
108 currentAction = EditorItemManager::MoveAndModify;
109 q->load(item);
110 }
111}
112
113void ItemEditorPrivate::itemFetchResult(KJob *job)
114{
115 Q_ASSERT(job);
116 Q_Q(EditorItemManager);
117
118 EditorItemManager::SaveAction action = currentAction;
119 currentAction = EditorItemManager::None;
120
121 if (job->error()) {
122 mItemUi->reject(ItemEditorUi::ItemFetchFailed, job->errorString());
123 return;
124 }
125
126 auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
127 if (fetchJob->items().isEmpty()) {
128 mItemUi->reject(ItemEditorUi::ItemFetchFailed);
129 return;
130 }
131
132 Akonadi::Item item = fetchJob->items().at(0);
133 if (mItemUi->hasSupportedPayload(item)) {
134 setItem(item);
135 if (action != EditorItemManager::None) {
136 // Finally enable ok/apply buttons, we've finished loading
137 Q_EMIT q->itemSaveFinished(action);
138 }
139 } else {
140 mItemUi->reject(ItemEditorUi::ItemHasInvalidPayload);
141 }
142}
143
144void ItemEditorPrivate::setItem(const Akonadi::Item &item)
145{
146 Q_ASSERT(item.hasPayload());
147 mPrevItem = item;
148 mItem = item;
149 mItemUi->load(item);
150 setupMonitor();
151}
152
153void ItemEditorPrivate::itemMoveResult(KJob *job)
154{
155 Q_ASSERT(job);
156 Q_Q(EditorItemManager);
157
158 if (job->error()) {
159 auto moveJob = qobject_cast<Akonadi::ItemMoveJob *>(job);
160 Q_ASSERT(moveJob);
161 Q_UNUSED(moveJob)
162 // Q_ASSERT(!moveJob->items().isEmpty());
163 // TODO: What is reasonable behavior at this point?
164 qCCritical(INCIDENCEEDITOR_LOG) << "Error while moving item "; // << moveJob->items().first().id() << " to collection "
165 //<< moveJob->destinationCollection() << job->errorString();
166 Q_EMIT q->itemSaveFailed(EditorItemManager::Move, job->errorString());
167 } else {
168 // Fetch the item again, we want a new mItem, which has an updated parentCollection
169 Akonadi::Item item(mItem.id());
170 // set currentAction, so the fetchResult slot emits itemSavedFinished(Move);
171 // We could emit it here, but we should only enable ok/apply buttons after the loading
172 // is complete
173 currentAction = EditorItemManager::Move;
174 q->load(item);
175 }
176}
177
178void ItemEditorPrivate::onModifyFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString)
179{
180 Q_Q(EditorItemManager);
181 if (resultCode == Akonadi::IncidenceChanger::ResultCodeSuccess) {
182 if (mItem.parentCollection() == mItemUi->selectedCollection() || mItem.storageCollectionId() == mItemUi->selectedCollection().id()) {
183 mItem = item;
184 Q_EMIT q->itemSaveFinished(EditorItemManager::Modify);
185 setupMonitor();
186 } else { // There's a collection move too.
187 auto moveJob = new Akonadi::ItemMoveJob(mItem, mItemUi->selectedCollection());
188 q->connect(moveJob, &KJob::result, q, [this](KJob *job) {
189 moveJobFinished(job);
190 });
191 }
192 } else if (resultCode == Akonadi::IncidenceChanger::ResultCodeUserCanceled) {
193 Q_EMIT q->itemSaveFailed(EditorItemManager::Modify, QString());
194 q->load(Akonadi::Item(mItem.id()));
195 } else {
196 qCCritical(INCIDENCEEDITOR_LOG) << "Modify failed " << errorString;
197 Q_EMIT q->itemSaveFailed(EditorItemManager::Modify, errorString);
198 }
199}
200
201void ItemEditorPrivate::onCreateFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString)
202{
203 Q_Q(EditorItemManager);
204 if (resultCode == Akonadi::IncidenceChanger::ResultCodeSuccess) {
205 currentAction = EditorItemManager::Create;
206 q->load(item);
207 setupMonitor();
208 } else {
209 qCCritical(INCIDENCEEDITOR_LOG) << "Creation failed " << errorString;
210 Q_EMIT q->itemSaveFailed(EditorItemManager::Create, errorString);
211 }
212}
213
214void ItemEditorPrivate::setupMonitor()
215{
216 // Q_Q(EditorItemManager);
217 delete mItemMonitor;
218 mItemMonitor = new Akonadi::Monitor;
219 mItemMonitor->setObjectName(QLatin1StringView("EditorItemManagerMonitor"));
220 mItemMonitor->ignoreSession(Akonadi::Session::defaultSession());
221 mItemMonitor->itemFetchScope().fetchFullPayload();
222 if (mItem.isValid()) {
223 mItemMonitor->setItemMonitored(mItem);
224 }
225
226 // q->connect(mItemMonitor, SIGNAL(itemChanged(Akonadi::Item,QSet<QByteArray>)),
227 // SLOT(itemChanged(Akonadi::Item,QSet<QByteArray>)));
228}
229
230void ItemEditorPrivate::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &partIdentifiers)
231{
232 Q_Q(EditorItemManager);
233 if (mItemUi->containsPayloadIdentifiers(partIdentifiers)) {
234 QPointer<QMessageBox> dlg = new QMessageBox; // krazy:exclude=qclasses
235 dlg->setIcon(QMessageBox::Question);
236 dlg->setInformativeText(
237 i18n("The item has been changed by another application.\n"
238 "What should be done?"));
239 dlg->addButton(i18n("Take over changes"), QMessageBox::AcceptRole);
240 dlg->addButton(i18n("Ignore and Overwrite changes"), QMessageBox::RejectRole);
241
242 if (dlg->exec() == QMessageBox::AcceptRole) {
243 auto job = new Akonadi::ItemFetchJob(mItem);
244 job->setFetchScope(mFetchScope);
245
246 mItem = item;
247
248 q->load(mItem);
249 } else {
250 mItem.setRevision(item.revision());
251 q->save();
252 }
253
254 delete dlg;
255 }
256
257 // Overwrite or not, we need to update the revision and the remote id to be able
258 // to store item later on.
259 mItem.setRevision(item.revision());
260}
261
262/// ItemEditor
263
264EditorItemManager::EditorItemManager(ItemEditorUi *ui, Akonadi::IncidenceChanger *changer)
265 : d_ptr(new ItemEditorPrivate(changer, this))
266{
268 d->mItemUi = ui;
269}
270
272
274{
275 Q_D(const ItemEditor);
276
277 switch (state) {
279 if (d->mItem.hasPayload()) {
280 return d->mItem;
281 } else {
282 qCDebug(INCIDENCEEDITOR_LOG) << "Won't return mItem because isValid = " << d->mItem.isValid() << "; and haPayload is " << d->mItem.hasPayload();
283 }
284 break;
286 if (d->mPrevItem.hasPayload()) {
287 return d->mPrevItem;
288 } else {
289 qCDebug(INCIDENCEEDITOR_LOG) << "Won't return mPrevItem because isValid = " << d->mPrevItem.isValid() << "; and haPayload is "
290 << d->mPrevItem.hasPayload();
291 }
292 break;
293 }
294 qCDebug(INCIDENCEEDITOR_LOG) << "state = " << state;
295 Q_ASSERT_X(false, "EditorItemManager::item", "Unknown enum value");
296 return {};
297}
298
300{
302
303 // We fetch anyways to make sure we have everything required including tags
304 auto job = new Akonadi::ItemFetchJob(item, this);
305 job->setFetchScope(d->mFetchScope);
306 connect(job, &KJob::result, this, [d](KJob *job) {
307 d->itemFetchResult(job);
308 });
309}
310
312{
314
315 if (!d->mItemUi->isValid()) {
316 Q_EMIT itemSaveFailed(d->mItem.isValid() ? Modify : Create, QString());
317 return;
318 }
319
320 if (!d->mItemUi->isDirty() && d->mItemUi->selectedCollection() == d->mItem.parentCollection()) {
321 // Item did not change and was not moved
322 Q_EMIT itemSaveFinished(None);
323 return;
324 }
325
326 d->mChanger->setGroupwareCommunication(CalendarSupport::KCalPrefs::instance()->useGroupwareCommunication());
327 updateIncidenceChangerPrivacyFlags(d->mChanger, itipPrivacy);
328
329 Akonadi::Item updateItem = d->mItemUi->save(d->mItem);
330 Q_ASSERT(updateItem.id() == d->mItem.id());
331 d->mItem = updateItem;
332
333 if (d->mItem.isValid()) { // A valid item. Means we're modifying.
334 Q_ASSERT(d->mItem.parentCollection().isValid());
336 if (d->mItem.parentCollection() == d->mItemUi->selectedCollection() || d->mItem.storageCollectionId() == d->mItemUi->selectedCollection().id()) {
337 (void)d->mChanger->modifyIncidence(d->mItem, oldPayload);
338 } else {
339 Q_ASSERT(d->mItemUi->selectedCollection().isValid());
340 Q_ASSERT(d->mItem.parentCollection().isValid());
341
342 qCDebug(INCIDENCEEDITOR_LOG) << "Moving from" << d->mItem.parentCollection().id() << "to" << d->mItemUi->selectedCollection().id();
343
344 if (d->mItemUi->isDirty()) {
345 (void)d->mChanger->modifyIncidence(d->mItem, oldPayload);
346 } else {
347 auto itemMoveJob = new Akonadi::ItemMoveJob(d->mItem, d->mItemUi->selectedCollection());
348 connect(itemMoveJob, &KJob::result, this, [d](KJob *job) {
349 d->itemMoveResult(job);
350 });
351 }
352 }
353 } else { // An invalid item. Means we're creating.
354 if (d->mIsCounterProposal) {
355 // We don't write back to akonadi, that will be done in ITipHandler.
356 Q_EMIT itemSaveFinished(EditorItemManager::Modify);
357 } else {
358 Q_ASSERT(d->mItemUi->selectedCollection().isValid());
359 (void)d->mChanger->createFromItem(d->mItem, d->mItemUi->selectedCollection());
360 }
361 }
362}
363
364void EditorItemManager::setIsCounterProposal(bool isCounterProposal)
365{
367 d->mIsCounterProposal = isCounterProposal;
368}
369
370ItemEditorUi::~ItemEditorUi() = default;
371
372bool ItemEditorUi::isValid() const
373{
374 return true;
375}
376} // namespace
377
378#include "moc_editoritemmanager.cpp"
void setRevision(int revision)
bool hasPayload() const
int revision() const
static Session * defaultSession()
Helper class for creating dialogs that let the user create and edit the payload of Akonadi items (e....
~EditorItemManager() override
Destructs the ItemEditor.
void load(const Akonadi::Item &item)
Loads the.
void save(ItipPrivacyFlags itipPrivacy=ItipPrivacyPlain)
Saves the new or modified item.
@ Modify
An existing item was modified.
Akonadi::Item item(ItemState state=AfterSave) const
Returns the last saved item with payload or an invalid item when save is not called yet.
@ AfterSave
Returns the last saved item.
@ BeforeSave
Returns an item with the original payload before the last save call.
virtual QString errorString() const
int error() const
void result(KJob *job)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T qobject_cast(QObject *object)
void setObjectName(QAnyStringView name)
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:37 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.