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