1 /*
2   SPDX-FileCopyrightText: 2010-2012 Sérgio Martins <iamsergio@gmail.com>
3 
4   SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "history.h"
8 #include "akonadicalendar_debug.h"
9 #include "history_p.h"
10 #include <KCalUtils/Stringify>
11 
12 using namespace KCalendarCore;
13 using namespace Akonadi;
14 
History(QObject * parent)15 History::History(QObject *parent)
16     : QObject(parent)
17     , d(new HistoryPrivate(this))
18 {
19 }
20 
21 History::~History() = default;
22 
HistoryPrivate(History * qq)23 HistoryPrivate::HistoryPrivate(History *qq)
24     : mChanger(new IncidenceChanger(/*history=*/false, qq))
25     , mOperationTypeInProgress(TypeNone)
26     , q(qq)
27 {
28     mChanger->setObjectName(QStringLiteral("changer")); // for auto-connects
29 }
30 
recordCreation(const Akonadi::Item & item,const QString & description,const uint atomicOperationId)31 void History::recordCreation(const Akonadi::Item &item, const QString &description, const uint atomicOperationId)
32 {
33     Q_ASSERT_X(item.isValid(), "History::recordCreation()", "Item must be valid.");
34 
35     Q_ASSERT_X(item.hasPayload<KCalendarCore::Incidence::Ptr>(), "History::recordCreation()", "Item must have Incidence::Ptr payload.");
36 
37     Entry::Ptr entry(new CreationEntry(item, description, this));
38 
39     d->stackEntry(entry, atomicOperationId);
40 }
41 
recordModification(const Akonadi::Item & oldItem,const Akonadi::Item & newItem,const QString & description,const uint atomicOperationId)42 void History::recordModification(const Akonadi::Item &oldItem, const Akonadi::Item &newItem, const QString &description, const uint atomicOperationId)
43 {
44     Q_ASSERT_X(oldItem.isValid(), "History::recordModification", "old item must be valid");
45     Q_ASSERT_X(newItem.isValid(), "History::recordModification", "newItem item must be valid");
46     Q_ASSERT_X(oldItem.hasPayload<KCalendarCore::Incidence::Ptr>(), "History::recordModification", "old item must have Incidence::Ptr payload");
47     Q_ASSERT_X(newItem.hasPayload<KCalendarCore::Incidence::Ptr>(), "History::recordModification", "newItem item must have Incidence::Ptr payload");
48 
49     Entry::Ptr entry(new ModificationEntry(newItem, oldItem.payload<KCalendarCore::Incidence::Ptr>(), description, this));
50 
51     Q_ASSERT(newItem.revision() >= oldItem.revision());
52 
53     d->stackEntry(entry, atomicOperationId);
54 }
55 
recordDeletion(const Akonadi::Item & item,const QString & description,const uint atomicOperationId)56 void History::recordDeletion(const Akonadi::Item &item, const QString &description, const uint atomicOperationId)
57 {
58     Q_ASSERT_X(item.isValid(), "History::recordDeletion", "Item must be valid");
59     Item::List list;
60     list.append(item);
61     recordDeletions(list, description, atomicOperationId);
62 }
63 
recordDeletions(const Akonadi::Item::List & items,const QString & description,const uint atomicOperationId)64 void History::recordDeletions(const Akonadi::Item::List &items, const QString &description, const uint atomicOperationId)
65 {
66     Entry::Ptr entry(new DeletionEntry(items, description, this));
67 
68     for (const Akonadi::Item &item : items) {
69         Q_UNUSED(item)
70         Q_ASSERT_X(item.isValid(), "History::recordDeletion()", "Item must be valid.");
71         Q_ASSERT_X(item.hasPayload<Incidence::Ptr>(), "History::recordDeletion()", "Item must have an Incidence::Ptr payload.");
72     }
73 
74     d->stackEntry(entry, atomicOperationId);
75 }
76 
nextUndoDescription() const77 QString History::nextUndoDescription() const
78 {
79     if (!d->mUndoStack.isEmpty()) {
80         return d->mUndoStack.top()->mDescription;
81     } else {
82         return QString();
83     }
84 }
85 
nextRedoDescription() const86 QString History::nextRedoDescription() const
87 {
88     if (!d->mRedoStack.isEmpty()) {
89         return d->mRedoStack.top()->mDescription;
90     } else {
91         return QString();
92     }
93 }
94 
undo(QWidget * parent)95 void History::undo(QWidget *parent)
96 {
97     d->undoOrRedo(TypeUndo, parent);
98 }
99 
redo(QWidget * parent)100 void History::redo(QWidget *parent)
101 {
102     d->undoOrRedo(TypeRedo, parent);
103 }
104 
undoAll(QWidget * parent)105 void History::undoAll(QWidget *parent)
106 {
107     if (d->mOperationTypeInProgress != TypeNone) {
108         qCWarning(AKONADICALENDAR_LOG) << "Don't call History::undoAll() while an undo/redo/undoAll is in progress";
109     } else if (d->mEnabled) {
110         d->mUndoAllInProgress = true;
111         d->mCurrentParent = parent;
112         d->doIt(TypeUndo);
113     } else {
114         qCWarning(AKONADICALENDAR_LOG) << "Don't call undo/redo when History is disabled";
115     }
116 }
117 
clear()118 bool History::clear()
119 {
120     bool result = true;
121     if (d->mOperationTypeInProgress == TypeNone) {
122         d->mRedoStack.clear();
123         d->mUndoStack.clear();
124         d->mLastErrorString.clear();
125         d->mQueuedEntries.clear();
126     } else {
127         result = false;
128     }
129     Q_EMIT changed();
130     return result;
131 }
132 
lastErrorString() const133 QString History::lastErrorString() const
134 {
135     return d->mLastErrorString;
136 }
137 
undoAvailable() const138 bool History::undoAvailable() const
139 {
140     return !d->mUndoStack.isEmpty() && d->mOperationTypeInProgress == TypeNone;
141 }
142 
redoAvailable() const143 bool History::redoAvailable() const
144 {
145     return !d->mRedoStack.isEmpty() && d->mOperationTypeInProgress == TypeNone;
146 }
147 
updateIds(Item::Id oldId,Item::Id newId)148 void HistoryPrivate::updateIds(Item::Id oldId, Item::Id newId)
149 {
150     mEntryInProgress->updateIds(oldId, newId);
151 
152     for (const Entry::Ptr &entry : std::as_const(mUndoStack)) {
153         entry->updateIds(oldId, newId);
154     }
155 
156     for (const Entry::Ptr &entry : std::as_const(mRedoStack)) {
157         entry->updateIds(oldId, newId);
158     }
159 }
160 
doIt(OperationType type)161 void HistoryPrivate::doIt(OperationType type)
162 {
163     mOperationTypeInProgress = type;
164     Q_EMIT q->changed(); // Application will disable undo/redo buttons because operation is in progress
165     Q_ASSERT(!stack().isEmpty());
166     mEntryInProgress = stack().pop();
167 
168     connect(mEntryInProgress.data(), &Entry::finished, this, &HistoryPrivate::handleFinished, Qt::UniqueConnection);
169     mEntryInProgress->doIt(type);
170 }
171 
handleFinished(IncidenceChanger::ResultCode changerResult,const QString & errorString)172 void HistoryPrivate::handleFinished(IncidenceChanger::ResultCode changerResult, const QString &errorString)
173 {
174     Q_ASSERT(mOperationTypeInProgress != TypeNone);
175     Q_ASSERT(!(mUndoAllInProgress && mOperationTypeInProgress == TypeRedo));
176 
177     const bool success = (changerResult == IncidenceChanger::ResultCodeSuccess);
178     const History::ResultCode resultCode = success ? History::ResultCodeSuccess : History::ResultCodeError;
179 
180     if (success) {
181         mLastErrorString.clear();
182         destinationStack().push(mEntryInProgress);
183     } else {
184         mLastErrorString = errorString;
185         stack().push(mEntryInProgress);
186     }
187 
188     mCurrentParent = nullptr;
189 
190     // Process recordCreation/Modification/Deletions that came in while an operation
191     // was in progress
192     if (!mQueuedEntries.isEmpty()) {
193         mRedoStack.clear();
194         for (const Entry::Ptr &entry : std::as_const(mQueuedEntries)) {
195             mUndoStack.push(entry);
196         }
197         mQueuedEntries.clear();
198     }
199 
200     emitDone(mOperationTypeInProgress, resultCode);
201     mOperationTypeInProgress = TypeNone;
202     Q_EMIT q->changed();
203 }
204 
stackEntry(const Entry::Ptr & entry,uint atomicOperationId)205 void HistoryPrivate::stackEntry(const Entry::Ptr &entry, uint atomicOperationId)
206 {
207     const bool useMultiEntry = (atomicOperationId > 0);
208 
209     Entry::Ptr entryToPush;
210 
211     if (useMultiEntry) {
212         Entry::Ptr topEntry = (mOperationTypeInProgress == TypeNone) ? (mUndoStack.isEmpty() ? Entry::Ptr() : mUndoStack.top())
213                                                                      : (mQueuedEntries.isEmpty() ? Entry::Ptr() : mQueuedEntries.last());
214 
215         const bool topIsMultiEntry = qobject_cast<MultiEntry *>(topEntry.data());
216 
217         if (topIsMultiEntry) {
218             MultiEntry::Ptr multiEntry = topEntry.staticCast<MultiEntry>();
219             if (multiEntry->mAtomicOperationId != atomicOperationId) {
220                 multiEntry = MultiEntry::Ptr(new MultiEntry(atomicOperationId, entry->mDescription, q));
221                 entryToPush = multiEntry;
222             }
223             multiEntry->addEntry(entry);
224         } else {
225             MultiEntry::Ptr multiEntry = MultiEntry::Ptr(new MultiEntry(atomicOperationId, entry->mDescription, q));
226             multiEntry->addEntry(entry);
227             entryToPush = multiEntry;
228         }
229     } else {
230         entryToPush = entry;
231     }
232 
233     if (mOperationTypeInProgress == TypeNone) {
234         if (entryToPush) {
235             mUndoStack.push(entryToPush);
236         }
237         mRedoStack.clear();
238         Q_EMIT q->changed();
239     } else {
240         if (entryToPush) {
241             mQueuedEntries.append(entryToPush);
242         }
243     }
244 }
245 
undoOrRedo(OperationType type,QWidget * parent)246 void HistoryPrivate::undoOrRedo(OperationType type, QWidget *parent)
247 {
248     // Don't call undo() without the previous one finishing
249     Q_ASSERT(mOperationTypeInProgress == TypeNone);
250 
251     if (!stack(type).isEmpty()) {
252         if (mEnabled) {
253             mCurrentParent = parent;
254             doIt(type);
255         } else {
256             qCWarning(AKONADICALENDAR_LOG) << "Don't call undo/redo when History is disabled";
257         }
258     } else {
259         qCWarning(AKONADICALENDAR_LOG) << "Don't call undo/redo when the stack is empty.";
260     }
261 }
262 
stack(OperationType type)263 QStack<Entry::Ptr> &HistoryPrivate::stack(OperationType type)
264 {
265     // Entries from the undo stack go to the redo stack, and vice-versa
266     return type == TypeUndo ? mUndoStack : mRedoStack;
267 }
268 
setEnabled(bool enabled)269 void HistoryPrivate::setEnabled(bool enabled)
270 {
271     mEnabled = enabled;
272 }
273 
redoCount() const274 int HistoryPrivate::redoCount() const
275 {
276     return mRedoStack.count();
277 }
278 
undoCount() const279 int HistoryPrivate::undoCount() const
280 {
281     return mUndoStack.count();
282 }
283 
stack()284 QStack<Entry::Ptr> &HistoryPrivate::stack()
285 {
286     return stack(mOperationTypeInProgress);
287 }
288 
destinationStack()289 QStack<Entry::Ptr> &HistoryPrivate::destinationStack()
290 {
291     // Entries from the undo stack go to the redo stack, and vice-versa
292     return mOperationTypeInProgress == TypeRedo ? mUndoStack : mRedoStack;
293 }
294 
emitDone(OperationType type,History::ResultCode resultCode)295 void HistoryPrivate::emitDone(OperationType type, History::ResultCode resultCode)
296 {
297     if (type == TypeUndo) {
298         Q_EMIT q->undone(resultCode);
299     } else if (type == TypeRedo) {
300         Q_EMIT q->redone(resultCode);
301     } else {
302         Q_ASSERT(false);
303     }
304 }
305 
incidenceChanger() const306 Akonadi::IncidenceChanger *History::incidenceChanger() const
307 {
308     return d->mChanger;
309 }
310 
311 #include "moc_history.cpp"
312 #include "moc_history_p.cpp"
313