File indexing completed on 2024-05-12 04:35:05
0001 /* This file is part of the TikZKit project. 0002 * 0003 * Copyright (C) 2015 Dominik Haumann <dhaumann@kde.org> 0004 * 0005 * This library is free software; you can redistribute it and/or modify 0006 * it under the terms of the GNU Library General Public License as published 0007 * by the Free Software Foundation, either version 2 of the License, or 0008 * (at your option) any later version. 0009 * 0010 * This library is distributed in the hope that it will be useful, 0011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0013 * GNU Library General Public License for more details. 0014 * 0015 * You should have received a copy of the GNU Library General Public License 0016 * along with this library; see the file COPYING.LIB. If not, see 0017 * <http://www.gnu.org/licenses/>. 0018 */ 0019 0020 #include "UndoManager.h" 0021 #include "Document.h" 0022 #include "UndoItem.h" 0023 #include "UndoGroup.h" 0024 0025 #include <QAction> 0026 #include <QIcon> 0027 #include <QPointer> 0028 0029 namespace tikz { 0030 namespace core { 0031 0032 class UndoManagerPrivate { 0033 public: 0034 /** 0035 * Pointer to the document of this undo/redo item. 0036 */ 0037 Document* doc = nullptr; 0038 0039 /** 0040 * Pointer to the clean undo group. 0041 */ 0042 UndoGroup * cleanUndoGroup = nullptr; 0043 0044 /** 0045 * Track previous clean state. 0046 */ 0047 bool wasClean = true; 0048 0049 /** 0050 * Holds all undo items. 0051 */ 0052 QList<UndoGroup *> undoItems; 0053 0054 /** 0055 * Holds all redo items. 0056 */ 0057 QList<UndoGroup *> redoItems; 0058 0059 /** 0060 * Holds the items of a currently pending transaction. 0061 */ 0062 UndoGroup * currentUndoGroup = nullptr; 0063 0064 /** 0065 * Ref-counting for balanced being/{cancel, commit} transactions. 0066 */ 0067 int transactionRefCount = 0; 0068 0069 /** 0070 * Flag that is set to true, if the currently pending transaction was 0071 * canceled. 0072 */ 0073 bool transactionCanceled = false; 0074 0075 public: // helper functions 0076 UndoGroup * groupForRow(int row) const 0077 { 0078 if (row < 0) { 0079 return nullptr; 0080 } 0081 0082 if (row < undoItems.size()) { 0083 // an undo item 0084 return undoItems[row]; 0085 } else if (row < undoItems.size() + redoItems.size()) { 0086 // a redo item 0087 return redoItems[row - undoItems.size()]; 0088 } 0089 0090 return nullptr; 0091 } 0092 }; 0093 0094 UndoManager::UndoManager(Document* doc) 0095 : QAbstractItemModel(doc) 0096 , d(new UndoManagerPrivate()) 0097 { 0098 d->doc = doc; 0099 } 0100 0101 UndoManager::~UndoManager() 0102 { 0103 clear(); 0104 0105 delete d; 0106 } 0107 0108 Document* UndoManager::document() 0109 { 0110 return d->doc; 0111 } 0112 0113 void UndoManager::setClean() 0114 { 0115 if (! isClean()) { 0116 d->cleanUndoGroup = d->undoItems.isEmpty() ? nullptr : d->undoItems.last(); 0117 Q_EMIT cleanChanged(true); 0118 } 0119 } 0120 0121 bool UndoManager::isClean() const 0122 { 0123 return d->cleanUndoGroup == (d->undoItems.isEmpty() ? nullptr : d->undoItems.last()); 0124 } 0125 0126 bool UndoManager::undoAvailable() const 0127 { 0128 return ! d->undoItems.isEmpty(); 0129 } 0130 0131 bool UndoManager::redoAvailable() const 0132 { 0133 return ! d->redoItems.isEmpty(); 0134 } 0135 0136 void UndoManager::undo() 0137 { 0138 if (! d->undoItems.isEmpty()) { 0139 const bool oldClean = isClean(); 0140 0141 // perform undo action 0142 const bool wasActive = document()->setUndoActive(true); 0143 d->undoItems.last()->undo(); 0144 document()->setUndoActive(wasActive); 0145 0146 d->redoItems.prepend(d->undoItems.last()); 0147 d->undoItems.removeLast(); 0148 0149 // send dataChanged() signal, since the current undo item is bold (needs repaint) 0150 const int startRow = d->undoItems.size(); 0151 QModelIndex startIndex = index(startRow, 0); 0152 QModelIndex endIndex = index(startRow + 1, 0); 0153 const QVector<int> roles {Qt::FontRole}; 0154 Q_EMIT dataChanged(startIndex, endIndex, roles); 0155 0156 // emit cleanChanged() if required 0157 const bool newClean = isClean(); 0158 if (oldClean != newClean) { 0159 Q_EMIT cleanChanged(newClean); 0160 } 0161 } 0162 } 0163 0164 void UndoManager::redo() 0165 { 0166 if (! d->redoItems.isEmpty()) { 0167 const bool oldClean = isClean(); 0168 0169 const bool wasActive = document()->setUndoActive(true); 0170 d->redoItems.first()->redo(); 0171 document()->setUndoActive(wasActive); 0172 0173 d->undoItems.append(d->redoItems.first()); 0174 d->redoItems.removeFirst(); 0175 0176 // send dataChanged() signal, since the current undo item is bold (needs repaint) 0177 const int startRow = d->undoItems.size() - 1; 0178 QModelIndex startIndex = index(startRow, 0); 0179 QModelIndex endIndex = index(startRow + 1, 0); 0180 const QVector<int> roles {Qt::FontRole}; 0181 Q_EMIT dataChanged(startIndex, endIndex, roles); 0182 0183 const bool newClean = isClean(); 0184 if (oldClean != newClean) { 0185 Q_EMIT cleanChanged(newClean); 0186 } 0187 } 0188 } 0189 0190 void UndoManager::clear() 0191 { 0192 beginResetModel(); 0193 0194 // a group for a clean state does not exist anymore 0195 d->cleanUndoGroup = nullptr; 0196 0197 // delete undo and redo items 0198 qDeleteAll(d->undoItems); 0199 qDeleteAll(d->redoItems); 0200 0201 d->undoItems.clear(); 0202 d->redoItems.clear(); 0203 0204 // delete/reset pending transaction data 0205 delete d->currentUndoGroup; 0206 d->currentUndoGroup = nullptr; 0207 d->transactionRefCount = 0; 0208 0209 endResetModel(); 0210 } 0211 0212 void UndoManager::addUndoItem(UndoItem * item) 0213 { 0214 Q_ASSERT(item); 0215 0216 // if a transaction was canceled, there is no pending undo group. 0217 // In this case, just delete the item. 0218 if (d->transactionRefCount > 0 && !d->currentUndoGroup) { 0219 delete item; 0220 return; 0221 } 0222 0223 startTransaction(); 0224 Q_ASSERT(d->currentUndoGroup != nullptr); 0225 0226 // execute redo action 0227 const bool wasActive = document()->setUndoActive(true); 0228 item->redo(); 0229 document()->setUndoActive(wasActive); 0230 0231 // add to current group. 0232 // WARNING: This might merge the item with an already existing item. 0233 // Therefore the item pointer may be a dangling pointer afterwards! 0234 d->currentUndoGroup->addItem(item); 0235 0236 commitTransaction(); 0237 } 0238 0239 QList<UndoGroup*> UndoManager::undoGroups() const 0240 { 0241 return d->undoItems; 0242 } 0243 0244 void UndoManager::startTransaction(const QString & text) 0245 { 0246 if (d->transactionRefCount == 0) { 0247 Q_ASSERT(d->currentUndoGroup == nullptr); 0248 0249 // create new pending undo group 0250 d->currentUndoGroup = new UndoGroup(text, this); 0251 0252 // track clean state at the beginning of the transaction 0253 d->wasClean = isClean(); 0254 0255 // set cancel-flag to false 0256 d->transactionCanceled = false; 0257 } 0258 0259 ++d->transactionRefCount; 0260 } 0261 0262 void UndoManager::cancelTransaction() 0263 { 0264 Q_ASSERT(d->transactionRefCount > 0); 0265 0266 // undo all, and delete group 0267 d->transactionCanceled = true; 0268 if (d->currentUndoGroup) { 0269 d->currentUndoGroup->undo(); 0270 delete d->currentUndoGroup; 0271 d->currentUndoGroup = nullptr; 0272 } 0273 } 0274 0275 void UndoManager::commitTransaction() 0276 { 0277 Q_ASSERT(d->transactionRefCount > 0); 0278 0279 --d->transactionRefCount; 0280 0281 // already balanced transaction ? 0282 if (d->transactionRefCount > 0) { 0283 return; 0284 } 0285 0286 // if the transaction was canceled, there is nothing to do. 0287 if (d->transactionCanceled) { 0288 Q_ASSERT(! d->currentUndoGroup); 0289 d->transactionCanceled = false; 0290 return; 0291 } 0292 0293 // calling startTransaction() immediately followed by commitTransaction() 0294 // will create an empty pending undo group. We don't want empty groups. 0295 if (d->currentUndoGroup && d->currentUndoGroup->isEmpty()) { 0296 delete d->currentUndoGroup; 0297 d->currentUndoGroup = nullptr; 0298 return; 0299 } 0300 0301 // 0302 // ok, now the real work: add to undo stack 0303 // 0304 const int rowIndex = d->undoItems.count(); 0305 0306 // first: clear redo items 0307 if (! d->redoItems.isEmpty()) { 0308 beginRemoveRows(index(rowIndex, 0), rowIndex, rowIndex + d->redoItems.count()); 0309 removeRows(rowIndex, d->redoItems.count()); 0310 qDeleteAll(d->redoItems); 0311 d->redoItems.clear(); 0312 endRemoveRows(); 0313 } 0314 0315 // next: add pending undo group 0316 { 0317 beginInsertRows(index(rowIndex, 0), rowIndex, rowIndex + 1); 0318 0319 insertRows(rowIndex, 1); 0320 d->undoItems.append(d->currentUndoGroup); 0321 d->currentUndoGroup = nullptr; 0322 0323 endInsertRows(); 0324 } 0325 0326 // track clean state 0327 if (d->wasClean) { 0328 Q_EMIT cleanChanged(false); 0329 } 0330 } 0331 0332 bool UndoManager::transactionActive() const 0333 { 0334 return d->transactionRefCount > 0; 0335 } 0336 0337 QModelIndex UndoManager::index(int row, int column, const QModelIndex & parent) const 0338 { 0339 // currently, we only support one column 0340 if (column != 0) { 0341 return QModelIndex(); 0342 } 0343 0344 // a top-level item is requested 0345 if (!parent.isValid()) { 0346 auto group = d->groupForRow(row); 0347 if (!group) { 0348 return QModelIndex(); 0349 } 0350 return createIndex(row, column, nullptr); 0351 } 0352 0353 // a child item of a top-level item is requrested 0354 UndoGroup * group = d->groupForRow(parent.row()); 0355 if (!group) { 0356 return QModelIndex(); 0357 } 0358 return createIndex(row, column, group); 0359 } 0360 0361 QModelIndex UndoManager::parent(const QModelIndex & index) const 0362 { 0363 if (!index.isValid()) { 0364 return QModelIndex(); 0365 } 0366 0367 auto group = static_cast<UndoGroup *>(index.internalPointer()); 0368 if (group) { 0369 // child item 0370 int row = d->undoItems.indexOf(group); 0371 if (row < 0) { 0372 row = d->redoItems.indexOf(group); 0373 Q_ASSERT(row >= 0); 0374 row += d->undoItems.size(); 0375 } 0376 return createIndex(row, 0, nullptr); 0377 } else { 0378 // top-level item 0379 return QModelIndex(); 0380 } 0381 } 0382 0383 int UndoManager::rowCount(const QModelIndex & parent) const 0384 { 0385 if (!parent.isValid()) { 0386 // top-level items 0387 return d->undoItems.size() + d->redoItems.size(); 0388 } else if (! parent.internalPointer()){ 0389 // child items 0390 auto group = d->groupForRow(parent.row()); 0391 if (group) { 0392 return group->count(); 0393 } 0394 } 0395 return 0; 0396 } 0397 0398 int UndoManager::columnCount(const QModelIndex & parent) const 0399 { 0400 if (!parent.isValid()) { 0401 return 1; 0402 } else { 0403 auto group = d->groupForRow(parent.row()); 0404 if (group) { 0405 return 1; 0406 } 0407 } 0408 return 0; 0409 } 0410 0411 QVariant UndoManager::data(const QModelIndex & index, int role) const 0412 { 0413 if (!index.isValid()) { 0414 return QVariant(); 0415 } 0416 0417 if (index.column() != 0) { 0418 return QVariant(); 0419 } 0420 0421 if (role == Qt::DisplayRole) { 0422 auto group = static_cast<UndoGroup *>(index.internalPointer()); 0423 if (group) { 0424 // child item 0425 return group->undoItems()[index.row()]->text(); 0426 } else { 0427 auto group = d->groupForRow(index.row()); 0428 Q_ASSERT(group); 0429 return group->text(); 0430 } 0431 } 0432 0433 if (role == Qt::FontRole) { 0434 auto group = static_cast<UndoGroup *>(index.internalPointer()); 0435 if (!group) { 0436 // top-level item 0437 if (index.row() == d->undoItems.size() - 1) { 0438 QFont font; 0439 font.setBold(true); 0440 return font; 0441 } 0442 } 0443 } 0444 0445 if (role == Qt::DecorationRole && d->cleanUndoGroup) { 0446 auto group = static_cast<UndoGroup *>(index.internalPointer()); 0447 if (!group) { 0448 group = d->groupForRow(index.row()); 0449 if (group == d->cleanUndoGroup) { 0450 return QColor(Qt::red); 0451 } 0452 } 0453 } 0454 0455 return QVariant(); 0456 } 0457 0458 bool UndoManager::insertRows(int row, int count, const QModelIndex & parent) 0459 { 0460 Q_UNUSED(row) 0461 Q_UNUSED(count) 0462 Q_UNUSED(parent) 0463 0464 // for now, inserting is only supported at top-level 0465 return true; 0466 } 0467 0468 bool UndoManager::removeRows(int row, int count, const QModelIndex & parent) 0469 { 0470 Q_UNUSED(row) 0471 Q_UNUSED(count) 0472 Q_UNUSED(parent) 0473 0474 // for now, inserting is only supported at top-level 0475 return true; 0476 } 0477 0478 void UndoManager::printTree() 0479 { 0480 qDebug() << "-- undo items --"; 0481 for (auto item : d->undoItems) { 0482 item->printTree(); 0483 } 0484 0485 qDebug() << "\n-- redo items --"; 0486 for (auto item : d->redoItems) { 0487 item->printTree(); 0488 } 0489 } 0490 0491 } 0492 } 0493 0494 // kate: indent-width 4; replace-tabs on;