1 /*
2  * document.cpp
3  * Copyright 2015, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
4  *
5  * This file is part of Tiled.
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "document.h"
22 
23 #include "changeevents.h"
24 #include "containerhelpers.h"
25 #include "editableasset.h"
26 #include "logginginterface.h"
27 #include "object.h"
28 #include "tile.h"
29 #include "wangset.h"
30 
31 #include <QFileInfo>
32 #include <QUndoStack>
33 
34 namespace Tiled {
35 
36 QHash<QString, Document*> Document::sDocumentInstances;
37 
Document(DocumentType type,const QString & fileName,QObject * parent)38 Document::Document(DocumentType type, const QString &fileName,
39                    QObject *parent)
40     : QObject(parent)
41     , mType(type)
42     , mFileName(fileName)
43     , mCanonicalFilePath(QFileInfo(mFileName).canonicalFilePath())
44     , mUndoStack(new QUndoStack(this))
45 {
46     if (!mCanonicalFilePath.isEmpty())
47         sDocumentInstances.insert(mCanonicalFilePath, this);
48 
49     connect(mUndoStack, &QUndoStack::cleanChanged, this, &Document::modifiedChanged);
50 }
51 
~Document()52 Document::~Document()
53 {
54     if (!mCanonicalFilePath.isEmpty()) {
55         auto i = sDocumentInstances.find(mCanonicalFilePath);
56         if (i != sDocumentInstances.end() && *i == this)
57             sDocumentInstances.erase(i);
58     }
59 }
60 
setFileName(const QString & fileName)61 void Document::setFileName(const QString &fileName)
62 {
63     if (mFileName == fileName)
64         return;
65 
66     QString oldFileName = mFileName;
67 
68     if (!mCanonicalFilePath.isEmpty()) {
69         auto i = sDocumentInstances.find(mCanonicalFilePath);
70         if (i != sDocumentInstances.end() && *i == this)
71             sDocumentInstances.erase(i);
72     }
73 
74     mFileName = fileName;
75     mCanonicalFilePath = QFileInfo(fileName).canonicalFilePath();
76 
77     if (!mCanonicalFilePath.isEmpty())
78         sDocumentInstances.insert(mCanonicalFilePath, this);
79 
80     emit fileNameChanged(fileName, oldFileName);
81 }
82 
checkFilePathProperties(const Object * object) const83 void Document::checkFilePathProperties(const Object *object) const
84 {
85     auto &props = object->properties();
86 
87     for (auto i = props.begin(), i_end = props.end(); i != i_end; ++i) {
88         if (i.value().userType() == filePathTypeId()) {
89             const QString localFile = i.value().value<FilePath>().url.toLocalFile();
90             if (!localFile.isEmpty() && !QFile::exists(localFile)) {
91                 WARNING(tr("Custom property '%1' refers to non-existing file '%2'").arg(i.key(), localFile),
92                         SelectCustomProperty { fileName(), i.key(), object},
93                         this);
94             }
95         }
96     }
97 }
98 
99 /**
100  * Returns whether the document has unsaved changes.
101  */
isModified() const102 bool Document::isModified() const
103 {
104     return !undoStack()->isClean();
105 }
106 
107 /**
108  * Sets the current \a object alongside the document owning that object.
109  *
110  * The owning document is necessary because the current object reference may
111  * need to be reset to prevent it from turning into a roaming pointer.
112  */
setCurrentObject(Object * object,Document * owningDocument)113 void Document::setCurrentObject(Object *object, Document *owningDocument)
114 {
115     if (object == mCurrentObject)
116         return;
117 
118     mCurrentObject = object;
119 
120     if (!object)
121         owningDocument = nullptr;
122 
123     if (mCurrentObjectDocument != owningDocument) {
124         if (mCurrentObjectDocument) {
125             disconnect(mCurrentObjectDocument, &QObject::destroyed, this, &Document::currentObjectDocumentDestroyed);
126             disconnect(mCurrentObjectDocument, &Document::changed, this, &Document::currentObjectDocumentChanged);
127         }
128         if (owningDocument) {
129             connect(owningDocument, &QObject::destroyed, this, &Document::currentObjectDocumentDestroyed);
130             connect(owningDocument, &Document::changed, this, &Document::currentObjectDocumentChanged);
131         }
132 
133         mCurrentObjectDocument = owningDocument;
134     }
135 
136     emit currentObjectChanged(object);
137 }
138 
139 /**
140  * Resets the current object when necessary.
141  *
142  * For some changes we'll need to reset the current object. At the moment, this
143  * function only handles those cases where the change comes from a different
144  * document. For example, the current object of a MapDocument might be a tile
145  * from a TilesetDocument. To avoid leaving a roaming pointer, it will need to
146  * be reset when that tile is removed.
147  */
currentObjectDocumentChanged(const ChangeEvent & change)148 void Document::currentObjectDocumentChanged(const ChangeEvent &change)
149 {
150     switch (change.type) {
151     case ChangeEvent::TilesAboutToBeRemoved: {
152         auto tilesEvent = static_cast<const TilesEvent&>(change);
153 
154         if (contains(tilesEvent.tiles, currentObject()))
155             setCurrentObject(nullptr);
156 
157         break;
158     }
159     case ChangeEvent::WangSetAboutToBeRemoved: {
160         auto wangSetEvent = static_cast<const WangSetEvent&>(change);
161         auto wangSet = wangSetEvent.tileset->wangSet(wangSetEvent.index);
162 
163         if (currentObject() == wangSet)
164             setCurrentObject(nullptr);
165         if (currentObject() && currentObject()->typeId() == Object::WangColorType)
166             if (static_cast<WangColor*>(currentObject())->wangSet() == wangSet)
167                 setCurrentObject(nullptr);
168 
169         break;
170     }
171     case ChangeEvent::WangColorAboutToBeRemoved: {
172         auto wangColorEvent = static_cast<const WangColorEvent&>(change);
173         auto wangColor = wangColorEvent.wangSet->colorAt(wangColorEvent.color);
174 
175         if (currentObject() == wangColor.data())
176             setCurrentObject(nullptr);
177 
178         break;
179     }
180     default:
181         break;
182     }
183 }
184 
currentObjectDocumentDestroyed()185 void Document::currentObjectDocumentDestroyed()
186 {
187     mCurrentObjectDocument = nullptr;   // don't need to disconnect from this
188     setCurrentObject(nullptr);
189 }
190 
currentObjects() const191 QList<Object *> Document::currentObjects() const
192 {
193     QList<Object*> objects;
194     if (mCurrentObject)
195         objects.append(mCurrentObject);
196     return objects;
197 }
198 
setProperty(Object * object,const QString & name,const QVariant & value)199 void Document::setProperty(Object *object,
200                            const QString &name,
201                            const QVariant &value)
202 {
203     const bool hadProperty = object->hasProperty(name);
204 
205     object->setProperty(name, value);
206 
207     if (hadProperty)
208         emit propertyChanged(object, name);
209     else
210         emit propertyAdded(object, name);
211 }
212 
setProperties(Object * object,const Properties & properties)213 void Document::setProperties(Object *object, const Properties &properties)
214 {
215     object->setProperties(properties);
216     emit propertiesChanged(object);
217 }
218 
removeProperty(Object * object,const QString & name)219 void Document::removeProperty(Object *object, const QString &name)
220 {
221     object->removeProperty(name);
222     emit propertyRemoved(object, name);
223 }
224 
setIgnoreBrokenLinks(bool ignoreBrokenLinks)225 void Document::setIgnoreBrokenLinks(bool ignoreBrokenLinks)
226 {
227     if (mIgnoreBrokenLinks == ignoreBrokenLinks)
228         return;
229 
230     mIgnoreBrokenLinks = ignoreBrokenLinks;
231     emit ignoreBrokenLinksChanged(ignoreBrokenLinks);
232 }
233 
setChangedOnDisk(bool changedOnDisk)234 void Document::setChangedOnDisk(bool changedOnDisk)
235 {
236     mChangedOnDisk = changedOnDisk;
237 }
238 
239 } // namespace Tiled
240 
241 #include "moc_document.cpp"
242