File indexing completed on 2024-05-12 04:35:03
0001 /* This file is part of the TikZKit project. 0002 * 0003 * Copyright (C) 2013-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 "Document.h" 0021 #include "Node.h" 0022 #include "EdgePath.h" 0023 #include "EllipsePath.h" 0024 #include "Style.h" 0025 0026 #include "Transaction.h" 0027 #include "UndoManager.h" 0028 #include "UndoFactory.h" 0029 #include "UndoGroup.h" 0030 #include "UndoCreateEntity.h" 0031 #include "UndoDeleteEntity.h" 0032 #include "UndoSetProperty.h" 0033 0034 #include "Visitor.h" 0035 #include "SerializeVisitor.h" 0036 #include "DeserializeVisitor.h" 0037 #include "TikzExportVisitor.h" 0038 0039 #include <QDebug> 0040 #include <QTextStream> 0041 #include <QFile> 0042 #include <QUrl> 0043 #include <QJsonArray> 0044 #include <QJsonDocument> 0045 #include <QJsonObject> 0046 0047 namespace tikz { 0048 namespace core { 0049 0050 // helper: remove \r and \n from visible document name (see Kate bug #170876) 0051 inline static QString removeNewLines(const QString &str) 0052 { 0053 QString tmp(str); 0054 return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")) 0055 .replace(QLatin1Char('\r'), QLatin1Char(' ')) 0056 .replace(QLatin1Char('\n'), QLatin1Char(' ')); 0057 } 0058 0059 class DocumentPrivate 0060 { 0061 public: 0062 // Document this private instance belongs to 0063 Document * q = nullptr; 0064 0065 // the Document's current url 0066 QUrl url; 0067 // undo manager 0068 UndoManager * undoManager = nullptr; 0069 // flag whether operations should add undo items or not 0070 bool undoActive = false; 0071 0072 Unit preferredUnit = Unit::Centimeter; 0073 0074 // global document style options 0075 Style * style = nullptr; 0076 0077 // Entity list, contains Nodes and Paths 0078 QVector<Entity *> entities; 0079 0080 // Node lookup map 0081 QHash<Uid, Entity *> entityMap; 0082 0083 // the document-wide unique ids start at 1. 0084 // Id 0 is reserved for the Document Uid, see Document constructor. 0085 qint64 nextId = 1; 0086 0087 // helper to get a document-wide unique id 0088 qint64 uniqueId() 0089 { 0090 return nextId++; 0091 } 0092 0093 QString docName = QString("Untitled"); 0094 0095 // 0096 // helper functions 0097 // 0098 public: 0099 void updateDocumentName() { 0100 if (! url.isEmpty() && docName == removeNewLines(url.fileName())) { 0101 return; 0102 } 0103 0104 QString newName = removeNewLines(url.fileName()); 0105 0106 if (newName.isEmpty()) { 0107 newName = "Untitled"; 0108 } 0109 0110 if (newName != docName) { 0111 docName = newName; 0112 Q_EMIT q->documentNameChanged(q); 0113 } 0114 } 0115 }; 0116 0117 Document::Document(QObject * parent) 0118 : Entity(Uid(0, this)) 0119 , d(new DocumentPrivate()) 0120 { 0121 // the Document's ownership is maintained elsewhere. Since the Entity 0122 // does not allow passing the ownership, we need to do this explicitly here. 0123 setParent(parent); 0124 0125 d->q = this; 0126 d->undoManager = new UndoManager(this); 0127 d->style = new Style(Uid(d->uniqueId(), this)); 0128 0129 // Debugging: 0130 d->style->setLineWidth(tikz::Value::veryThick()); 0131 0132 connect(d->undoManager, SIGNAL(cleanChanged(bool)), this, SIGNAL(modifiedChanged())); 0133 } 0134 0135 Document::~Document() 0136 { 0137 // clear Document contents 0138 close(); 0139 0140 // make sure things are really gone 0141 Q_ASSERT(d->entityMap.isEmpty()); 0142 Q_ASSERT(d->entities.isEmpty()); 0143 0144 delete d; 0145 } 0146 0147 tikz::EntityType Document::entityType() const 0148 { 0149 return tikz::EntityType::Document; 0150 } 0151 0152 bool Document::accept(Visitor & visitor) 0153 { 0154 // visit this document 0155 visitor.visit(this); 0156 0157 // visit all styles 0158 for (auto entity : d->entities) { 0159 auto style = qobject_cast<Style *>(entity); 0160 if (style) { 0161 style->accept(visitor); 0162 } 0163 } 0164 0165 // visit all nodes 0166 for (auto entity : d->entities) { 0167 auto node = qobject_cast<Node *>(entity); 0168 if (node) { 0169 node->accept(visitor); 0170 } 0171 } 0172 0173 // visit all paths 0174 for (auto entity : d->entities) { 0175 auto path = qobject_cast<Path *>(entity); 0176 if (path) { 0177 path->accept(visitor); 0178 } 0179 } 0180 0181 return true; 0182 } 0183 0184 void Document::close() 0185 { 0186 // tell the world that all Nodes and Paths are about to be deleted 0187 Q_EMIT aboutToClear(); 0188 0189 // free all node and path data 0190 qDeleteAll(d->entities); 0191 d->entities.clear(); 0192 d->entityMap.clear(); 0193 0194 // reset unique id counter 0195 d->nextId = 1; 0196 0197 // reinitialize document style 0198 delete d->style; 0199 d->style = new Style(Uid(d->uniqueId(), this)); 0200 0201 // clear undo stack 0202 d->undoManager->clear(); 0203 0204 // unnamed document 0205 d->url.clear(); 0206 0207 // keep the document name up-to-date 0208 d->updateDocumentName(); 0209 0210 // propagate change() signal from style 0211 connect(d->style, &ConfigObject::changed, this, &ConfigObject::emitChangedIfNeeded); 0212 } 0213 0214 bool Document::load(const QUrl & fileurl) 0215 { 0216 // first start a clean document 0217 close(); 0218 0219 // open file + read all json contents 0220 QFile file(fileurl.toLocalFile()); 0221 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 0222 return false; 0223 } 0224 0225 QJsonDocument json = QJsonDocument::fromJson(file.readAll()); 0226 QJsonObject root = json.object(); 0227 0228 // read history and replay 0229 UndoFactory factory(this); 0230 QJsonArray history = root["history"].toArray(); 0231 for (auto action : history) { 0232 QJsonObject entry = action.toObject(); 0233 Transaction transaction(this, entry["text"].toString()); 0234 QJsonArray items = entry["items"].toArray(); 0235 for (auto item : items) { 0236 QJsonObject joItem = item.toObject(); 0237 const QString type = joItem["type"].toString(); 0238 UndoItem * undoItem = factory.createItem(type); 0239 if (undoItem) { 0240 undoItem->load(joItem); 0241 addUndoItem(undoItem); 0242 } 0243 } 0244 } 0245 0246 if (root.contains("preferred-unit")) { 0247 setPreferredUnit(toEnum<Unit>(root["preferred-unit"].toString())); 0248 } 0249 0250 // now make sure the next free uniq id is valid by finding the maximum 0251 // used id, and then add "+1". 0252 auto keys = d->entityMap.keys(); 0253 if (keys.size()) { 0254 d->nextId = std::max_element(keys.begin(), keys.end())->id() + 1; 0255 } 0256 0257 // keep the document name up-to-date 0258 d->updateDocumentName(); 0259 0260 // mark this state as unmodified 0261 d->undoManager->setClean(); 0262 0263 return true; 0264 } 0265 0266 bool Document::reload() 0267 { 0268 if (!d->url.isEmpty()) { 0269 return load(d->url); 0270 } 0271 return false; 0272 } 0273 0274 bool Document::save() 0275 { 0276 return saveAs(d->url); 0277 } 0278 0279 bool Document::saveAs(const QUrl & targetUrl) 0280 { 0281 SerializeVisitor v; 0282 accept(v); 0283 v.save(targetUrl.path()); 0284 return true; 0285 0286 const bool urlChanged = d->url.toLocalFile() != targetUrl.toLocalFile(); 0287 0288 if (targetUrl.isLocalFile()) { 0289 0290 // first serialize to json document 0291 QJsonArray jsonHistory; 0292 for (auto group : d->undoManager->undoGroups()) { 0293 QJsonArray groupItems; 0294 for (auto item : group->undoItems()) { 0295 groupItems.append(item->save()); 0296 } 0297 0298 QJsonObject jsonGroup; 0299 jsonGroup["text"] = group->text(); 0300 jsonGroup["items"] = groupItems; 0301 jsonHistory.append(jsonGroup); 0302 } 0303 0304 QJsonObject json; 0305 json["history"] = jsonHistory; 0306 json["preferred-unit"] = toString(preferredUnit()); 0307 0308 // now save data 0309 QFile file(targetUrl.toLocalFile()); 0310 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { 0311 return false; 0312 } 0313 0314 // write json to text stream 0315 QTextStream stream(&file); 0316 QJsonDocument jsonDoc(json); 0317 stream << jsonDoc.toJson(); 0318 0319 if (urlChanged) { 0320 d->url = targetUrl; 0321 // keep the document name up-to-date 0322 d->updateDocumentName(); 0323 } 0324 0325 // mark this state as unmodified 0326 d->undoManager->setClean(); 0327 0328 return true; 0329 } 0330 0331 return false; 0332 } 0333 0334 QUrl Document::url() const 0335 { 0336 return d->url; 0337 } 0338 0339 QString Document::documentName() const 0340 { 0341 return d->docName; 0342 } 0343 0344 bool Document::isEmptyBuffer() const 0345 { 0346 return d->url.isEmpty() 0347 && ! isModified() 0348 && d->entities.isEmpty(); 0349 } 0350 0351 QString Document::tikzCode() 0352 { 0353 TikzExportVisitor tev; 0354 accept(tev); 0355 0356 return tev.tikzCode(); 0357 } 0358 0359 void Document::addUndoItem(tikz::core::UndoItem * undoItem) 0360 { 0361 d->undoManager->addUndoItem(undoItem); 0362 } 0363 0364 void Document::beginTransaction(const QString & name) 0365 { 0366 // track changes 0367 beginConfig(); 0368 0369 // pass call to undo mananger 0370 d->undoManager->startTransaction(name); 0371 } 0372 0373 void Document::cancelTransaction() 0374 { 0375 d->undoManager->cancelTransaction(); 0376 } 0377 0378 void Document::finishTransaction() 0379 { 0380 // first pass call to undo mananger 0381 d->undoManager->commitTransaction(); 0382 0383 // notify world about changes 0384 endConfig(); 0385 } 0386 0387 bool Document::transactionRunning() const 0388 { 0389 return d->undoManager->transactionActive(); 0390 } 0391 0392 bool Document::setUndoActive(bool active) 0393 { 0394 const bool lastState = d->undoActive; 0395 d->undoActive = active; 0396 return lastState; 0397 } 0398 0399 bool Document::undoActive() const 0400 { 0401 return d->undoActive; 0402 } 0403 0404 bool Document::isModified() const 0405 { 0406 return ! d->undoManager->isClean(); 0407 } 0408 0409 bool Document::undoAvailable() const 0410 { 0411 return d->undoManager->undoAvailable(); 0412 } 0413 0414 bool Document::redoAvailable() const 0415 { 0416 return d->undoManager->redoAvailable(); 0417 } 0418 0419 QAbstractItemModel * Document::historyModel() const 0420 { 0421 return d->undoManager; 0422 } 0423 0424 void Document::undo() 0425 { 0426 const bool undoWasAvailable = undoAvailable(); 0427 const bool redoWasAvailable = redoAvailable(); 0428 0429 d->undoManager->undo(); 0430 0431 const bool undoNowAvailable = undoAvailable(); 0432 const bool redoNowAvailable = redoAvailable(); 0433 0434 if (undoWasAvailable != undoNowAvailable) { 0435 Q_EMIT undoAvailableChanged(undoNowAvailable); 0436 } 0437 0438 if (redoWasAvailable != redoNowAvailable) { 0439 Q_EMIT redoAvailableChanged(redoNowAvailable); 0440 } 0441 } 0442 0443 void Document::redo() 0444 { 0445 const bool undoWasAvailable = undoAvailable(); 0446 const bool redoWasAvailable = redoAvailable(); 0447 0448 d->undoManager->redo(); 0449 0450 const bool undoNowAvailable = undoAvailable(); 0451 const bool redoNowAvailable = redoAvailable(); 0452 0453 if (undoWasAvailable != undoNowAvailable) { 0454 Q_EMIT undoAvailableChanged(undoNowAvailable); 0455 } 0456 0457 if (redoWasAvailable != redoNowAvailable) { 0458 Q_EMIT redoAvailableChanged(redoNowAvailable); 0459 } 0460 } 0461 0462 tikz::Pos Document::scenePos(const MetaPos & pos) const 0463 { 0464 const auto node = pos.node(); 0465 if (!node) { 0466 return pos.pos(); 0467 } 0468 0469 return node->pos(); 0470 } 0471 0472 void Document::setPreferredUnit(tikz::Unit unit) 0473 { 0474 if (d->preferredUnit != unit) { 0475 d->preferredUnit = unit; 0476 Q_EMIT preferredUnitChanged(d->preferredUnit); 0477 } 0478 } 0479 0480 tikz::Unit Document::preferredUnit() const 0481 { 0482 return d->preferredUnit; 0483 } 0484 0485 Style * Document::style() const 0486 { 0487 return d->style; 0488 } 0489 0490 QVector<Uid> Document::nodes() const 0491 { 0492 QVector <Uid> nodeList; 0493 auto it = d->entityMap.cbegin(); 0494 while (it != d->entityMap.cend()) { 0495 if (qobject_cast<Node *>(it.value())) { 0496 nodeList.append(it.key()); 0497 } 0498 ++it; 0499 } 0500 return nodeList; 0501 } 0502 0503 QVector<Uid> Document::paths() const 0504 { 0505 QVector <Uid> pathList; 0506 auto it = d->entityMap.cbegin(); 0507 while (it != d->entityMap.cend()) { 0508 if (qobject_cast<Path *>(it.value())) { 0509 pathList.append(it.key()); 0510 } 0511 ++it; 0512 } 0513 return pathList; 0514 } 0515 0516 Entity * Document::createEntity(tikz::EntityType type) 0517 { 0518 // create new node, push will call ::redo() 0519 const Uid uid(d->uniqueId(), this); 0520 addUndoItem(new UndoCreateEntity(uid, type, this)); 0521 0522 // now the node should be in the map 0523 const auto it = d->entityMap.find(uid); 0524 if (it != d->entityMap.end()) { 0525 return *it; 0526 } 0527 0528 // requested id not in map, this is a bug, since UndoCreateEntity should 0529 // call createEntity(uid, type) that inserts the Entity 0530 Q_ASSERT(false); 0531 0532 return nullptr; 0533 } 0534 0535 Entity * Document::createEntity(const Uid & uid, EntityType type) 0536 { 0537 Q_ASSERT(uid.isValid()); 0538 Q_ASSERT(uid.document() == this); 0539 Q_ASSERT(!d->entityMap.contains(uid)); 0540 0541 // create new node 0542 Entity * e = nullptr; 0543 switch (type) { 0544 case EntityType::Document: Q_ASSERT(false); break; 0545 case EntityType::Style: { 0546 e = new Style(uid); 0547 e->setObjectName("Style " + uid.toString()); 0548 ((Style*)e)->setParentStyle(style()->uid()); 0549 break; 0550 } 0551 case EntityType::Node: { 0552 e = new Node(uid); 0553 e->setObjectName("Node " + uid.toString()); 0554 break; 0555 } 0556 case EntityType::Path: { 0557 e = new EdgePath(PathType::Line, uid); // FIXME: only EdgePath right now 0558 e->setObjectName("Path " + uid.toString()); 0559 break; 0560 } 0561 } 0562 0563 Q_ASSERT(e); 0564 d->entities.append(e); 0565 0566 // insert entity into hash map 0567 d->entityMap.insert(uid, e); 0568 0569 // propagate changed signal 0570 connect(e, &ConfigObject::changed, this, &ConfigObject::emitChangedIfNeeded); 0571 0572 return e; 0573 } 0574 0575 void Document::deleteEntity(Entity * e) 0576 { 0577 // valid input? 0578 Q_ASSERT(e != nullptr); 0579 Q_ASSERT(d->entityMap.contains(e->uid())); 0580 0581 // get id 0582 const Uid uid = e->uid(); 0583 0584 // start undo group 0585 d->undoManager->startTransaction("Remove entity"); 0586 0587 // make sure no edge points to the deleted node 0588 if (auto nodeEntity = qobject_cast<Node*>(e)) { 0589 for (auto entity : d->entities) { 0590 if (auto path = qobject_cast<Path *>(entity)) { 0591 path->detachFromNode(nodeEntity); 0592 } 0593 0594 // TODO: a path might require the node? 0595 // in that case, maybe delete the path as well? 0596 } 0597 } 0598 0599 // delete node, push will call ::redo() 0600 addUndoItem(new UndoDeleteEntity(uid, this)); 0601 0602 // end undo group 0603 d->undoManager->commitTransaction(); 0604 0605 // node really removed? 0606 Q_ASSERT(!d->entityMap.contains(uid)); 0607 } 0608 0609 void Document::deleteEntity(const Uid & uid) 0610 { 0611 // valid input? 0612 Q_ASSERT(uid.isValid()); 0613 Q_ASSERT(d->entityMap.contains(uid)); 0614 0615 // get entity 0616 auto it = d->entityMap.find(uid); 0617 if (it != d->entityMap.end()) { 0618 const auto entity = *it; 0619 0620 // unregister entity 0621 d->entityMap.erase(it); 0622 Q_ASSERT(d->entities.contains(entity)); 0623 d->entities.erase(std::find(d->entities.begin(), d->entities.end(), entity)); 0624 0625 // truly delete node 0626 delete entity; 0627 } 0628 } 0629 0630 Node * Document::createNode() 0631 { 0632 Transaction transaction(this, "Create Node"); 0633 0634 // create node style 0635 auto nodeStyle = createEntity(tikz::EntityType::Style); 0636 0637 // create node 0638 auto node = createEntity<Node>(tikz::EntityType::Node); 0639 0640 // set the node style 0641 addUndoItem(new UndoSetProperty(node->uid(), "style", nodeStyle->uid())); 0642 0643 return node; 0644 } 0645 0646 Path * Document::createPath() 0647 { 0648 Transaction transaction(this, "Create Path"); 0649 0650 // create path style 0651 auto pathStyle = createEntity(tikz::EntityType::Style); 0652 0653 // create path 0654 auto path = createEntity<Path>(tikz::EntityType::Path); 0655 0656 // set the path style 0657 addUndoItem(new UndoSetProperty(path->uid(), "style", pathStyle->uid())); 0658 0659 return path; 0660 } 0661 0662 Path * Document::createPath(PathType type, const Uid & uid) 0663 { 0664 Q_ASSERT(uid.isValid()); 0665 0666 // create new path 0667 Path* path = nullptr; 0668 switch(type) { 0669 case PathType::Line: 0670 case PathType::HVLine: 0671 case PathType::VHLine: 0672 case PathType::BendCurve: 0673 case PathType::InOutCurve: 0674 case PathType::BezierCurve: { 0675 path = new EdgePath(type, uid); 0676 break; 0677 } 0678 case PathType::Ellipse: 0679 path = new EllipsePath(uid); 0680 break; 0681 default: 0682 Q_ASSERT(false); 0683 } 0684 0685 // register path 0686 d->entities.append(path); 0687 0688 // insert path into hash map 0689 d->entityMap.insert(uid, path); 0690 0691 // propagate changed signal 0692 connect(path, &ConfigObject::changed, this, &ConfigObject::emitChangedIfNeeded); 0693 0694 return path; 0695 } 0696 0697 Entity * Document::entity(const tikz::core::Uid & uid) const 0698 { 0699 if (uid.document() != this) { 0700 return nullptr; 0701 } 0702 0703 // Uid 0 alreay refers to this Document 0704 if (uid.id() == 0) { 0705 return const_cast<Document*>(this); 0706 } 0707 0708 // Uid 1 alreay refers to this Document's Style 0709 if (uid.id() == 1) { 0710 return d->style; 0711 } 0712 0713 // all other entities are in the entity list 0714 const auto it = d->entityMap.find(uid); 0715 if (it != d->entityMap.end()) { 0716 return *it; 0717 } 0718 0719 return nullptr; 0720 } 0721 0722 QVector<Uid> Document::entities() const 0723 { 0724 return QVector<Uid>::fromList(d->entityMap.keys()); 0725 } 0726 0727 0728 } 0729 } 0730 0731 // kate: indent-width 4; replace-tabs on;