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

KDE's Doxygen guidelines are available online.