1 /*
2  * tilesetdocument.cpp
3  * Copyright 2015-2016, 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 "tilesetdocument.h"
22 
23 #include "changeevents.h"
24 #include "editabletileset.h"
25 #include "issuesmodel.h"
26 #include "map.h"
27 #include "mapdocument.h"
28 #include "tile.h"
29 #include "tilesetformat.h"
30 #include "tilesetwangsetmodel.h"
31 #include "wangcolormodel.h"
32 #include "wangset.h"
33 
34 #include <QCoreApplication>
35 #include <QFileInfo>
36 #include <QUndoStack>
37 
38 namespace Tiled {
39 
40 class ReloadTileset : public QUndoCommand
41 {
42 public:
ReloadTileset(TilesetDocument * tilesetDocument,const SharedTileset & tileset)43     ReloadTileset(TilesetDocument *tilesetDocument, const SharedTileset &tileset)
44         : mTilesetDocument(tilesetDocument)
45         , mTileset(tileset)
46     {
47         setText(QCoreApplication::translate("Undo Commands", "Reload Tileset"));
48     }
49 
undo()50     void undo() override { mTilesetDocument->swapTileset(mTileset); }
redo()51     void redo() override { mTilesetDocument->swapTileset(mTileset); }
52 
53 private:
54     TilesetDocument *mTilesetDocument;
55     SharedTileset mTileset;
56 };
57 
58 
59 QMap<SharedTileset, TilesetDocument*> TilesetDocument::sTilesetToDocument;
60 
TilesetDocument(const SharedTileset & tileset)61 TilesetDocument::TilesetDocument(const SharedTileset &tileset)
62     : Document(TilesetDocumentType, tileset->fileName())
63     , mTileset(tileset)
64     , mWangSetModel(new TilesetWangSetModel(this, this))
65 {
66     Q_ASSERT(!sTilesetToDocument.contains(tileset));
67     sTilesetToDocument.insert(tileset, this);
68 
69     mCurrentObject = tileset.data();
70 
71     connect(this, &TilesetDocument::propertyAdded,
72             this, &TilesetDocument::onPropertyAdded);
73     connect(this, &TilesetDocument::propertyRemoved,
74             this, &TilesetDocument::onPropertyRemoved);
75     connect(this, &TilesetDocument::propertyChanged,
76             this, &TilesetDocument::onPropertyChanged);
77     connect(this, &TilesetDocument::propertiesChanged,
78             this, &TilesetDocument::onPropertiesChanged);
79 
80     connect(mWangSetModel, &TilesetWangSetModel::wangSetRemoved,
81             this, &TilesetDocument::onWangSetRemoved);
82 }
83 
~TilesetDocument()84 TilesetDocument::~TilesetDocument()
85 {
86     // Clear any previously found issues in this document
87     IssuesModel::instance().removeIssuesWithContext(this);
88 
89     sTilesetToDocument.remove(mTileset);
90 
91     // Needs to be deleted before the Tileset instance is deleted, because it
92     // may cause script values to detach from the map, in which case they'll
93     // need to be able to copy the data.
94     mEditable.reset();
95 }
96 
save(const QString & fileName,QString * error)97 bool TilesetDocument::save(const QString &fileName, QString *error)
98 {
99     auto tilesetFormat = findFileFormat<TilesetFormat>(mTileset->format(), FileFormat::Write);;
100     if (!tilesetFormat) {
101         if (error)
102             *error = tr("Tileset format '%s' not found").arg(mTileset->format());
103         return false;
104     }
105 
106     if (!tilesetFormat->write(*tileset(), fileName)) {
107         if (error)
108             *error = tilesetFormat->errorString();
109         return false;
110     }
111 
112     undoStack()->setClean();
113 
114     if (mTileset->fileName() != fileName) {
115         mTileset->setFileName(fileName);
116         mTileset->exportFileName.clear();
117     }
118 
119     setFileName(fileName);
120 
121     mLastSaved = QFileInfo(fileName).lastModified();
122 
123     emit saved();
124     return true;
125 }
126 
canReload() const127 bool TilesetDocument::canReload() const
128 {
129     return !fileName().isEmpty() && !mTileset->format().isEmpty();
130 }
131 
reload(QString * error)132 bool TilesetDocument::reload(QString *error)
133 {
134     if (!canReload())
135         return false;
136 
137     auto format = findFileFormat<TilesetFormat>(mTileset->format(), FileFormat::Read);
138     if (!format) {
139         if (error)
140             *error = tr("Tileset format '%s' not found").arg(mTileset->format());
141         return false;
142     }
143 
144     SharedTileset tileset = format->read(fileName());
145 
146     if (tileset.isNull()) {
147         if (error)
148             *error = format->errorString();
149         return false;
150     }
151 
152     tileset->setFileName(fileName());
153     tileset->setFormat(format->shortName());
154 
155     undoStack()->push(new ReloadTileset(this, tileset));
156     undoStack()->setClean();
157     mLastSaved = QFileInfo(fileName()).lastModified();
158 
159     return true;
160 }
161 
load(const QString & fileName,TilesetFormat * format,QString * error)162 TilesetDocumentPtr TilesetDocument::load(const QString &fileName,
163                                          TilesetFormat *format,
164                                          QString *error)
165 {
166     SharedTileset tileset = format->read(fileName);
167 
168     if (tileset.isNull()) {
169         if (error)
170             *error = format->errorString();
171         return TilesetDocumentPtr();
172     }
173 
174     tileset->setFileName(fileName);
175     tileset->setFormat(format->shortName());
176 
177     return TilesetDocumentPtr::create(tileset);
178 }
179 
writerFormat() const180 TilesetFormat *TilesetDocument::writerFormat() const
181 {
182     return findFileFormat<TilesetFormat>(mTileset->format(), FileFormat::Write);
183 }
184 
setWriterFormat(TilesetFormat * format)185 void TilesetDocument::setWriterFormat(TilesetFormat *format)
186 {
187     Q_ASSERT(format->hasCapabilities(FileFormat::Write));
188     mTileset->setFormat(format->shortName());
189 }
190 
lastExportFileName() const191 QString TilesetDocument::lastExportFileName() const
192 {
193     return tileset()->exportFileName;
194 }
195 
setLastExportFileName(const QString & fileName)196 void TilesetDocument::setLastExportFileName(const QString &fileName)
197 {
198     tileset()->exportFileName = fileName;
199 }
200 
exportFormat() const201 TilesetFormat* TilesetDocument::exportFormat() const
202 {
203     return findFileFormat<TilesetFormat>(tileset()->exportFormat);
204 }
205 
setExportFormat(FileFormat * format)206 void TilesetDocument::setExportFormat(FileFormat *format)
207 {
208     Q_ASSERT(qobject_cast<TilesetFormat*>(format));
209     tileset()->exportFormat = format->shortName();
210 }
211 
displayName() const212 QString TilesetDocument::displayName() const
213 {
214     QString displayName;
215 
216     if (isEmbedded()) {
217         displayName = mMapDocuments.first()->displayName();
218         displayName += QLatin1Char('#');
219         displayName += mTileset->name();
220     } else {
221         displayName = QFileInfo(fileName()).fileName();
222         if (displayName.isEmpty())
223             displayName = tr("untitled.tsx");
224     }
225 
226     return displayName;
227 }
228 
externalOrEmbeddedFileName() const229 QString TilesetDocument::externalOrEmbeddedFileName() const
230 {
231     QString result;
232 
233     if (isEmbedded()) {
234         result = mMapDocuments.first()->fileName();
235         result += QLatin1Char('#');
236         result += mTileset->name();
237     } else {
238         result = fileName();
239     }
240 
241     return result;
242 }
243 
244 /**
245  * Exchanges the tileset data of the tileset wrapped by this document with the
246  * data in the given \a tileset, and vice-versa.
247  */
swapTileset(SharedTileset & tileset)248 void TilesetDocument::swapTileset(SharedTileset &tileset)
249 {
250     // Bring pointers to safety
251     setSelectedTiles(QList<Tile*>());
252     setCurrentObject(mTileset.data());
253     mEditable.reset();
254 
255     sTilesetToDocument.remove(mTileset);
256     mTileset->swap(*tileset);
257     sTilesetToDocument.insert(mTileset, this);
258 
259     emit tilesetChanged(mTileset.data());
260 }
261 
editable()262 EditableTileset *TilesetDocument::editable()
263 {
264     if (!mEditable)
265         mEditable.reset(new EditableTileset(this, this));
266 
267     return static_cast<EditableTileset*>(mEditable.get());
268 }
269 
270 /**
271  * Used when a map that has this tileset embedded is saved.
272  */
setClean()273 void TilesetDocument::setClean()
274 {
275     undoStack()->setClean();
276 }
277 
addMapDocument(MapDocument * mapDocument)278 void TilesetDocument::addMapDocument(MapDocument *mapDocument)
279 {
280     Q_ASSERT(!mMapDocuments.contains(mapDocument));
281     mMapDocuments.append(mapDocument);
282 }
283 
removeMapDocument(MapDocument * mapDocument)284 void TilesetDocument::removeMapDocument(MapDocument *mapDocument)
285 {
286     Q_ASSERT(mMapDocuments.contains(mapDocument));
287     mMapDocuments.removeOne(mapDocument);
288 }
289 
setTilesetName(const QString & name)290 void TilesetDocument::setTilesetName(const QString &name)
291 {
292     mTileset->setName(name);
293     emit tilesetNameChanged(mTileset.data());
294 
295     for (MapDocument *mapDocument : mapDocuments())
296         emit mapDocument->tilesetNameChanged(mTileset.data());
297 }
298 
setTilesetTileOffset(QPoint tileOffset)299 void TilesetDocument::setTilesetTileOffset(QPoint tileOffset)
300 {
301     mTileset->setTileOffset(tileOffset);
302 
303     // Invalidate the draw margins of the maps using this tileset
304     for (MapDocument *mapDocument : mapDocuments())
305         mapDocument->map()->invalidateDrawMargins();
306 
307     emit tilesetTileOffsetChanged(mTileset.data());
308 
309     for (MapDocument *mapDocument : mapDocuments())
310         emit mapDocument->tilesetTilePositioningChanged(mTileset.data());
311 }
312 
setTilesetObjectAlignment(Alignment objectAlignment)313 void TilesetDocument::setTilesetObjectAlignment(Alignment objectAlignment)
314 {
315     mTileset->setObjectAlignment(objectAlignment);
316 
317     emit tilesetObjectAlignmentChanged(mTileset.data());
318 
319     for (MapDocument *mapDocument : mapDocuments())
320         emit mapDocument->tilesetTilePositioningChanged(mTileset.data());
321 }
322 
setTilesetTransformationFlags(Tileset::TransformationFlags flags)323 void TilesetDocument::setTilesetTransformationFlags(Tileset::TransformationFlags flags)
324 {
325     tileset()->setTransformationFlags(flags);
326     emit tilesetChanged(mTileset.data());
327 }
328 
addTiles(const QList<Tile * > & tiles)329 void TilesetDocument::addTiles(const QList<Tile *> &tiles)
330 {
331     mTileset->addTiles(tiles);
332     emit tilesAdded(tiles);
333     emit tilesetChanged(mTileset.data());
334 }
335 
removeTiles(const QList<Tile * > & tiles)336 void TilesetDocument::removeTiles(const QList<Tile *> &tiles)
337 {
338     // Switch current object to the tileset when it is one of the removed tiles
339     for (Tile *tile : tiles) {
340         if (tile == currentObject()) {
341             setCurrentObject(mTileset.data());
342             break;
343         }
344     }
345 
346     emit changed(TilesEvent(ChangeEvent::TilesAboutToBeRemoved, tiles));
347     mTileset->removeTiles(tiles);
348     emit tilesRemoved(tiles);
349     emit tilesetChanged(mTileset.data());
350 }
351 
352 /**
353  * \sa Tileset::relocateTiles
354  */
relocateTiles(const QList<Tile * > & tiles,int location)355 QList<int> TilesetDocument::relocateTiles(const QList<Tile *> &tiles, int location)
356 {
357     const auto prevLocations = mTileset->relocateTiles(tiles, location);
358     emit tilesetChanged(mTileset.data());
359     return prevLocations;
360 }
361 
setSelectedTiles(const QList<Tile * > & selectedTiles)362 void TilesetDocument::setSelectedTiles(const QList<Tile*> &selectedTiles)
363 {
364     mSelectedTiles = selectedTiles;
365     emit selectedTilesChanged();
366 }
367 
currentObjects() const368 QList<Object *> TilesetDocument::currentObjects() const
369 {
370     if (mCurrentObject->typeId() == Object::TileType && !mSelectedTiles.isEmpty()) {
371         QList<Object*> objects;
372         for (Tile *tile : mSelectedTiles)
373             objects.append(tile);
374         return objects;
375     }
376 
377     return Document::currentObjects();
378 }
379 
380 /**
381  * Returns the WangColorModel instance for the given \a wangSet.
382  * The model instances are created on-demand and owned by the document.
383  */
wangColorModel(WangSet * wangSet)384 WangColorModel *TilesetDocument::wangColorModel(WangSet *wangSet)
385 {
386     Q_ASSERT(wangSet->tileset() == mTileset.data());
387 
388     std::unique_ptr<WangColorModel> &model = mWangColorModels[wangSet];
389     if (!model)
390         model = std::make_unique<WangColorModel>(this, wangSet);
391     return model.get();
392 }
393 
setTileType(Tile * tile,const QString & type)394 void TilesetDocument::setTileType(Tile *tile, const QString &type)
395 {
396     Q_ASSERT(tile->tileset() == mTileset.data());
397 
398     tile->setType(type);
399     emit tileTypeChanged(tile);
400 
401     for (MapDocument *mapDocument : mapDocuments())
402         emit mapDocument->tileTypeChanged(tile);
403 }
404 
setTileImage(Tile * tile,const QPixmap & image,const QUrl & source)405 void TilesetDocument::setTileImage(Tile *tile, const QPixmap &image, const QUrl &source)
406 {
407     Q_ASSERT(tile->tileset() == mTileset.data());
408 
409     mTileset->setTileImage(tile, image, source);
410     emit tileImageSourceChanged(tile);
411 
412     for (MapDocument *mapDocument : mapDocuments())
413         emit mapDocument->tileImageSourceChanged(tile);
414 }
415 
setTileProbability(Tile * tile,qreal probability)416 void TilesetDocument::setTileProbability(Tile *tile, qreal probability)
417 {
418     Q_ASSERT(tile->tileset() == mTileset.data());
419 
420     tile->setProbability(probability);
421     emit tileProbabilityChanged(tile);
422 
423     for (MapDocument *mapDocument : mapDocuments())
424         emit mapDocument->tileProbabilityChanged(tile);
425 }
426 
swapTileObjectGroup(Tile * tile,std::unique_ptr<ObjectGroup> & objectGroup)427 void TilesetDocument::swapTileObjectGroup(Tile *tile, std::unique_ptr<ObjectGroup> &objectGroup)
428 {
429     tile->swapObjectGroup(objectGroup);
430     emit tileObjectGroupChanged(tile);
431 
432     for (MapDocument *mapDocument : mapDocuments())
433         emit mapDocument->tileObjectGroupChanged(tile);
434 }
435 
checkIssues()436 void TilesetDocument::checkIssues()
437 {
438     // Clear any previously found issues in this document
439     IssuesModel::instance().removeIssuesWithContext(this);
440 
441     if (tileset()->imageStatus() == LoadingError) {
442         auto fileName = tileset()->imageSource().toString(QUrl::PreferLocalFile);
443         ERROR(tr("Failed to load tileset image '%1'").arg(fileName),
444               std::function<void()>(), this);       // todo: hook to file dialog
445     }
446 
447     checkFilePathProperties(tileset().data());
448 
449     for (Tile *tile : tileset()->tiles()) {
450         checkFilePathProperties(tile);
451         // todo: check properties on collision objects
452 
453         if (!tile->imageSource().isEmpty() && tile->imageStatus() == LoadingError) {
454             auto fileName = tile->imageSource().toString(QUrl::PreferLocalFile);
455             ERROR(tr("Failed to load tile image '%1'").arg(fileName),
456                   std::function<void()>(), this);   // todo: hook to file dialog
457         }
458     }
459     for (WangSet *wangSet : tileset()->wangSets()) {
460         checkFilePathProperties(wangSet);
461         // todo: check properties on wang colors
462     }
463 }
464 
findDocumentForTileset(const SharedTileset & tileset)465 TilesetDocument *TilesetDocument::findDocumentForTileset(const SharedTileset &tileset)
466 {
467     return sTilesetToDocument.value(tileset);
468 }
469 
onPropertyAdded(Object * object,const QString & name)470 void TilesetDocument::onPropertyAdded(Object *object, const QString &name)
471 {
472     for (MapDocument *mapDocument : mapDocuments())
473         emit mapDocument->propertyAdded(object, name);
474 }
475 
onPropertyRemoved(Object * object,const QString & name)476 void TilesetDocument::onPropertyRemoved(Object *object, const QString &name)
477 {
478     for (MapDocument *mapDocument : mapDocuments())
479         emit mapDocument->propertyRemoved(object, name);
480 }
481 
onPropertyChanged(Object * object,const QString & name)482 void TilesetDocument::onPropertyChanged(Object *object, const QString &name)
483 {
484     for (MapDocument *mapDocument : mapDocuments())
485         emit mapDocument->propertyChanged(object, name);
486 }
487 
onPropertiesChanged(Object * object)488 void TilesetDocument::onPropertiesChanged(Object *object)
489 {
490     for (MapDocument *mapDocument : mapDocuments())
491         emit mapDocument->propertiesChanged(object);
492 }
493 
onWangSetRemoved(WangSet * wangSet)494 void TilesetDocument::onWangSetRemoved(WangSet *wangSet)
495 {
496     mWangColorModels.erase(wangSet);
497 }
498 
499 } // namespace Tiled
500 
501 #include "moc_tilesetdocument.cpp"
502