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