File indexing completed on 2024-05-12 05:10:44

0001 /*
0002   SPDX-FileCopyrightText: 2010-2012 Sérgio Martins <iamsergio@gmail.com>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "history.h"
0008 #include "akonadicalendar_debug.h"
0009 #include "history_p.h"
0010 
0011 using namespace KCalendarCore;
0012 using namespace Akonadi;
0013 
0014 History::History(QObject *parent)
0015     : QObject(parent)
0016     , d(new HistoryPrivate(this))
0017 {
0018 }
0019 
0020 History::~History() = default;
0021 
0022 HistoryPrivate::HistoryPrivate(History *qq)
0023     : mChanger(new IncidenceChanger(/*history=*/false, qq))
0024     , mOperationTypeInProgress(TypeNone)
0025     , q(qq)
0026 {
0027     mChanger->setObjectName(QLatin1StringView("changer")); // for auto-connects
0028 }
0029 
0030 void History::recordCreation(const Akonadi::Item &item, const QString &description, const uint atomicOperationId)
0031 {
0032     Q_ASSERT_X(item.isValid(), "History::recordCreation()", "Item must be valid.");
0033 
0034     Q_ASSERT_X(item.hasPayload<KCalendarCore::Incidence::Ptr>(), "History::recordCreation()", "Item must have Incidence::Ptr payload.");
0035 
0036     Entry::Ptr entry(new CreationEntry(item, description, this));
0037 
0038     d->stackEntry(entry, atomicOperationId);
0039 }
0040 
0041 void History::recordModification(const Akonadi::Item &oldItem, const Akonadi::Item &newItem, const QString &description, const uint atomicOperationId)
0042 {
0043     Q_ASSERT_X(oldItem.isValid(), "History::recordModification", "old item must be valid");
0044     Q_ASSERT_X(newItem.isValid(), "History::recordModification", "newItem item must be valid");
0045     Q_ASSERT_X(oldItem.hasPayload<KCalendarCore::Incidence::Ptr>(), "History::recordModification", "old item must have Incidence::Ptr payload");
0046     Q_ASSERT_X(newItem.hasPayload<KCalendarCore::Incidence::Ptr>(), "History::recordModification", "newItem item must have Incidence::Ptr payload");
0047 
0048     Entry::Ptr entry(new ModificationEntry(newItem, oldItem.payload<KCalendarCore::Incidence::Ptr>(), description, this));
0049 
0050     Q_ASSERT(newItem.revision() >= oldItem.revision());
0051 
0052     d->stackEntry(entry, atomicOperationId);
0053 }
0054 
0055 void History::recordDeletion(const Akonadi::Item &item, const QString &description, const uint atomicOperationId)
0056 {
0057     Q_ASSERT_X(item.isValid(), "History::recordDeletion", "Item must be valid");
0058     Item::List list;
0059     list.append(item);
0060     recordDeletions(list, description, atomicOperationId);
0061 }
0062 
0063 void History::recordDeletions(const Akonadi::Item::List &items, const QString &description, const uint atomicOperationId)
0064 {
0065     Entry::Ptr entry(new DeletionEntry(items, description, this));
0066 
0067     for (const Akonadi::Item &item : items) {
0068         Q_UNUSED(item)
0069         Q_ASSERT_X(item.isValid(), "History::recordDeletion()", "Item must be valid.");
0070         Q_ASSERT_X(item.hasPayload<Incidence::Ptr>(), "History::recordDeletion()", "Item must have an Incidence::Ptr payload.");
0071     }
0072 
0073     d->stackEntry(entry, atomicOperationId);
0074 }
0075 
0076 QString History::nextUndoDescription() const
0077 {
0078     if (!d->mUndoStack.isEmpty()) {
0079         return d->mUndoStack.top()->mDescription;
0080     } else {
0081         return {};
0082     }
0083 }
0084 
0085 QString History::nextRedoDescription() const
0086 {
0087     if (!d->mRedoStack.isEmpty()) {
0088         return d->mRedoStack.top()->mDescription;
0089     } else {
0090         return {};
0091     }
0092 }
0093 
0094 void History::undo(QWidget *parent)
0095 {
0096     d->undoOrRedo(TypeUndo, parent);
0097 }
0098 
0099 void History::redo(QWidget *parent)
0100 {
0101     d->undoOrRedo(TypeRedo, parent);
0102 }
0103 
0104 void History::undoAll(QWidget *parent)
0105 {
0106     if (d->mOperationTypeInProgress != TypeNone) {
0107         qCWarning(AKONADICALENDAR_LOG) << "Don't call History::undoAll() while an undo/redo/undoAll is in progress";
0108     } else if (d->mEnabled) {
0109         d->mUndoAllInProgress = true;
0110         d->mCurrentParent = parent;
0111         d->doIt(TypeUndo);
0112     } else {
0113         qCWarning(AKONADICALENDAR_LOG) << "Don't call undo/redo when History is disabled";
0114     }
0115 }
0116 
0117 bool History::clear()
0118 {
0119     bool result = true;
0120     if (d->mOperationTypeInProgress == TypeNone) {
0121         d->mRedoStack.clear();
0122         d->mUndoStack.clear();
0123         d->mLastErrorString.clear();
0124         d->mQueuedEntries.clear();
0125     } else {
0126         result = false;
0127     }
0128     Q_EMIT changed();
0129     return result;
0130 }
0131 
0132 QString History::lastErrorString() const
0133 {
0134     return d->mLastErrorString;
0135 }
0136 
0137 bool History::undoAvailable() const
0138 {
0139     return !d->mUndoStack.isEmpty() && d->mOperationTypeInProgress == TypeNone;
0140 }
0141 
0142 bool History::redoAvailable() const
0143 {
0144     return !d->mRedoStack.isEmpty() && d->mOperationTypeInProgress == TypeNone;
0145 }
0146 
0147 void HistoryPrivate::updateIds(Item::Id oldId, Item::Id newId)
0148 {
0149     mEntryInProgress->updateIds(oldId, newId);
0150 
0151     for (const Entry::Ptr &entry : std::as_const(mUndoStack)) {
0152         entry->updateIds(oldId, newId);
0153     }
0154 
0155     for (const Entry::Ptr &entry : std::as_const(mRedoStack)) {
0156         entry->updateIds(oldId, newId);
0157     }
0158 }
0159 
0160 void HistoryPrivate::doIt(OperationType type)
0161 {
0162     mOperationTypeInProgress = type;
0163     Q_EMIT q->changed(); // Application will disable undo/redo buttons because operation is in progress
0164     Q_ASSERT(!stack().isEmpty());
0165     mEntryInProgress = stack().pop();
0166 
0167     connect(mEntryInProgress.data(), &Entry::finished, this, &HistoryPrivate::handleFinished, Qt::UniqueConnection);
0168     mEntryInProgress->doIt(type);
0169 }
0170 
0171 void HistoryPrivate::handleFinished(IncidenceChanger::ResultCode changerResult, const QString &errorString)
0172 {
0173     Q_ASSERT(mOperationTypeInProgress != TypeNone);
0174     Q_ASSERT(!(mUndoAllInProgress && mOperationTypeInProgress == TypeRedo));
0175 
0176     const bool success = (changerResult == IncidenceChanger::ResultCodeSuccess);
0177     const History::ResultCode resultCode = success ? History::ResultCodeSuccess : History::ResultCodeError;
0178 
0179     if (success) {
0180         mLastErrorString.clear();
0181         destinationStack().push(mEntryInProgress);
0182     } else {
0183         mLastErrorString = errorString;
0184         stack().push(mEntryInProgress);
0185     }
0186 
0187     mCurrentParent = nullptr;
0188 
0189     // Process recordCreation/Modification/Deletions that came in while an operation
0190     // was in progress
0191     if (!mQueuedEntries.isEmpty()) {
0192         mRedoStack.clear();
0193         for (const Entry::Ptr &entry : std::as_const(mQueuedEntries)) {
0194             mUndoStack.push(entry);
0195         }
0196         mQueuedEntries.clear();
0197     }
0198 
0199     emitDone(mOperationTypeInProgress, resultCode);
0200     mOperationTypeInProgress = TypeNone;
0201     Q_EMIT q->changed();
0202 }
0203 
0204 void HistoryPrivate::stackEntry(const Entry::Ptr &entry, uint atomicOperationId)
0205 {
0206     const bool useMultiEntry = (atomicOperationId > 0);
0207 
0208     Entry::Ptr entryToPush;
0209 
0210     if (useMultiEntry) {
0211         Entry::Ptr topEntry = (mOperationTypeInProgress == TypeNone) ? (mUndoStack.isEmpty() ? Entry::Ptr() : mUndoStack.top())
0212                                                                      : (mQueuedEntries.isEmpty() ? Entry::Ptr() : mQueuedEntries.last());
0213 
0214         const bool topIsMultiEntry = qobject_cast<MultiEntry *>(topEntry.data());
0215 
0216         if (topIsMultiEntry) {
0217             MultiEntry::Ptr multiEntry = topEntry.staticCast<MultiEntry>();
0218             if (multiEntry->mAtomicOperationId != atomicOperationId) {
0219                 multiEntry = MultiEntry::Ptr(new MultiEntry(atomicOperationId, entry->mDescription, q));
0220                 entryToPush = multiEntry;
0221             }
0222             multiEntry->addEntry(entry);
0223         } else {
0224             MultiEntry::Ptr multiEntry = MultiEntry::Ptr(new MultiEntry(atomicOperationId, entry->mDescription, q));
0225             multiEntry->addEntry(entry);
0226             entryToPush = multiEntry;
0227         }
0228     } else {
0229         entryToPush = entry;
0230     }
0231 
0232     if (mOperationTypeInProgress == TypeNone) {
0233         if (entryToPush) {
0234             mUndoStack.push(entryToPush);
0235         }
0236         mRedoStack.clear();
0237         Q_EMIT q->changed();
0238     } else {
0239         if (entryToPush) {
0240             mQueuedEntries.append(entryToPush);
0241         }
0242     }
0243 }
0244 
0245 void HistoryPrivate::undoOrRedo(OperationType type, QWidget *parent)
0246 {
0247     // Don't call undo() without the previous one finishing
0248     Q_ASSERT(mOperationTypeInProgress == TypeNone);
0249 
0250     if (!stack(type).isEmpty()) {
0251         if (mEnabled) {
0252             mCurrentParent = parent;
0253             doIt(type);
0254         } else {
0255             qCWarning(AKONADICALENDAR_LOG) << "Don't call undo/redo when History is disabled";
0256         }
0257     } else {
0258         qCWarning(AKONADICALENDAR_LOG) << "Don't call undo/redo when the stack is empty.";
0259     }
0260 }
0261 
0262 QStack<Entry::Ptr> &HistoryPrivate::stack(OperationType type)
0263 {
0264     // Entries from the undo stack go to the redo stack, and vice-versa
0265     return type == TypeUndo ? mUndoStack : mRedoStack;
0266 }
0267 
0268 void HistoryPrivate::setEnabled(bool enabled)
0269 {
0270     mEnabled = enabled;
0271 }
0272 
0273 int HistoryPrivate::redoCount() const
0274 {
0275     return mRedoStack.count();
0276 }
0277 
0278 int HistoryPrivate::undoCount() const
0279 {
0280     return mUndoStack.count();
0281 }
0282 
0283 QStack<Entry::Ptr> &HistoryPrivate::stack()
0284 {
0285     return stack(mOperationTypeInProgress);
0286 }
0287 
0288 QStack<Entry::Ptr> &HistoryPrivate::destinationStack()
0289 {
0290     // Entries from the undo stack go to the redo stack, and vice-versa
0291     return mOperationTypeInProgress == TypeRedo ? mUndoStack : mRedoStack;
0292 }
0293 
0294 void HistoryPrivate::emitDone(OperationType type, History::ResultCode resultCode)
0295 {
0296     if (type == TypeUndo) {
0297         Q_EMIT q->undone(resultCode);
0298     } else if (type == TypeRedo) {
0299         Q_EMIT q->redone(resultCode);
0300     } else {
0301         Q_ASSERT(false);
0302     }
0303 }
0304 
0305 Akonadi::IncidenceChanger *History::incidenceChanger() const
0306 {
0307     return d->mChanger;
0308 }
0309 
0310 #include "moc_history.cpp"