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;