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"