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