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;