1 /*
2  * brokenlinks.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 "brokenlinks.h"
22 
23 #include "changetileimagesource.h"
24 #include "documentmanager.h"
25 #include "fileformat.h"
26 #include "mainwindow.h"
27 #include "map.h"
28 #include "objectgroup.h"
29 #include "replacetemplate.h"
30 #include "replacetileset.h"
31 #include "session.h"
32 #include "templatemanager.h"
33 #include "templatesdock.h"
34 #include "tile.h"
35 #include "tilesetchanges.h"
36 #include "tilesetdocument.h"
37 #include "tilesetmanager.h"
38 #include "tmxmapformat.h"
39 #include "utils.h"
40 
41 #include <QPushButton>
42 #include <QDialogButtonBox>
43 #include <QFileDialog>
44 #include <QFileInfo>
45 #include <QHeaderView>
46 #include <QImageReader>
47 #include <QLabel>
48 #include <QMessageBox>
49 #include <QSortFilterProxyModel>
50 #include <QStackedLayout>
51 #include <QTreeView>
52 
53 #include <algorithm>
54 
55 namespace Tiled {
56 
filePath() const57 QString BrokenLink::filePath() const
58 {
59     switch (type) {
60     case TilesetImageSource:
61         return _tileset->imageSource().toString(QUrl::PreferLocalFile);
62     case MapTilesetReference:
63         return _tileset->fileName();
64     case ObjectTemplateTilesetReference:
65         return _objectTemplate->object()->cell().tileset()->fileName();
66     case TilesetTileImageSource:
67         return _tile->imageSource().toString(QUrl::PreferLocalFile);
68     case ObjectTemplateReference:
69         return _objectTemplate->fileName();
70     }
71 
72     return QString();
73 }
74 
tileset() const75 Tileset *BrokenLink::tileset() const
76 {
77     switch (type) {
78     case TilesetImageSource:
79     case MapTilesetReference:
80         return _tileset;
81     case TilesetTileImageSource:
82         return _tile->tileset();
83     case ObjectTemplateReference:
84     case ObjectTemplateTilesetReference:
85         return nullptr;
86     }
87 
88     return nullptr;
89 }
90 
objectTemplate() const91 const ObjectTemplate *BrokenLink::objectTemplate() const
92 {
93     return (type == ObjectTemplateReference ||
94             type == ObjectTemplateTilesetReference) ? _objectTemplate : nullptr;
95 }
96 
BrokenLinksModel(QObject * parent)97 BrokenLinksModel::BrokenLinksModel(QObject *parent)
98     : QAbstractListModel(parent)
99     , mDocument(nullptr)
100 {
101 }
102 
setDocument(Document * document)103 void BrokenLinksModel::setDocument(Document *document)
104 {
105     if (auto mapDocument = qobject_cast<MapDocument*>(mDocument)) {
106         mapDocument->disconnect(this);
107 
108         for (const SharedTileset &tileset : mapDocument->map()->tilesets())
109             disconnectFromTileset(tileset);
110 
111     } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(mDocument)) {
112         disconnectFromTileset(tilesetDocument->tileset());
113     }
114 
115     mDocument = document;
116     refresh();
117 
118     if (mDocument) {
119         if (auto mapDocument = qobject_cast<MapDocument*>(mDocument)) {
120             connect(mapDocument, &MapDocument::tilesetAdded,
121                     this, &BrokenLinksModel::tilesetAdded);
122             connect(mapDocument, &MapDocument::tilesetRemoved,
123                     this, &BrokenLinksModel::tilesetRemoved);
124             connect(mapDocument, &MapDocument::objectTemplateReplaced,
125                     this, &BrokenLinksModel::refresh);
126 
127             for (const SharedTileset &tileset : mapDocument->map()->tilesets())
128                 connectToTileset(tileset);
129 
130             connect(DocumentManager::instance(), &DocumentManager::templateTilesetReplaced,
131                     this, &BrokenLinksModel::refresh);
132         } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(mDocument)) {
133             connectToTileset(tilesetDocument->tileset());
134         }
135 
136         connect(mDocument, &Document::ignoreBrokenLinksChanged,
137                 this, &BrokenLinksModel::refresh);
138     }
139 }
140 
refresh()141 void BrokenLinksModel::refresh()
142 {
143     if (mDocument)
144         mDocument->checkIssues();
145 
146     bool brokenLinksBefore = hasBrokenLinks();
147 
148     beginResetModel();
149 
150     mBrokenLinks.clear();
151 
152     if (mDocument && !mDocument->ignoreBrokenLinks()) {
153         QSet<SharedTileset> processedTilesets;
154 
155         auto processTileset = [this,&processedTilesets](const SharedTileset &tileset) {
156             if (processedTilesets.contains(tileset))
157                 return;
158 
159             processedTilesets.insert(tileset);
160 
161             if (tileset->isCollection()) {
162                 for (Tile *tile : tileset->tiles()) {
163                     if (!tile->imageSource().isEmpty() && tile->imageStatus() == LoadingError) {
164                         BrokenLink link;
165                         link.type = TilesetTileImageSource;
166                         link._tile = tile;
167                         mBrokenLinks.append(link);
168                     }
169                 }
170             } else {
171                 if (tileset->imageStatus() == LoadingError) {
172                     BrokenLink link;
173                     link.type = TilesetImageSource;
174                     link._tileset = tileset.data();
175                     mBrokenLinks.append(link);
176                 }
177             }
178         };
179 
180         if (auto mapDocument = qobject_cast<MapDocument*>(mDocument)) {
181             for (const SharedTileset &tileset : mapDocument->map()->tilesets()) {
182                 if (!tileset->fileName().isEmpty() && tileset->status() == LoadingError) {
183                     BrokenLink link;
184                     link.type = MapTilesetReference;
185                     link._tileset = tileset.data();
186                     mBrokenLinks.append(link);
187                 } else {
188                     processTileset(tileset);
189                 }
190             }
191 
192             QSet<const ObjectTemplate*> brokenTemplates;
193             QSet<const ObjectTemplate*> brokenTemplateTilesets;
194 
195             auto processTemplate = [&](const ObjectTemplate *objectTemplate){
196                 if (auto object = objectTemplate->object()) {
197                     if (auto tileset = object->cell().tileset()) {
198                         if (!tileset->fileName().isEmpty() && tileset->status() == LoadingError) {
199                             brokenTemplateTilesets.insert(objectTemplate);
200                         } else {
201                             processTileset(tileset->sharedPointer());
202                         }
203                     }
204                 } else {
205                     brokenTemplates.insert(objectTemplate);
206                 }
207             };
208 
209             LayerIterator it(mapDocument->map());
210             while (Layer *layer = it.next()) {
211                 if (ObjectGroup *objectGroup = layer->asObjectGroup()) {
212                     for (MapObject *mapObject : *objectGroup) {
213                         if (const ObjectTemplate *objectTemplate = mapObject->objectTemplate())
214                             processTemplate(objectTemplate);
215                     }
216                 }
217             }
218 
219             for (const ObjectTemplate *objectTemplate : brokenTemplates) {
220                 BrokenLink link;
221                 link.type = ObjectTemplateReference;
222                 link._objectTemplate = objectTemplate;
223                 mBrokenLinks.append(link);
224             }
225 
226             for (const ObjectTemplate *objectTemplate : brokenTemplateTilesets) {
227                 BrokenLink link;
228                 link.type = ObjectTemplateTilesetReference;
229                 link._objectTemplate = objectTemplate;
230                 mBrokenLinks.append(link);
231             }
232 
233         } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(mDocument)) {
234             processTileset(tilesetDocument->tileset());
235         }
236     }
237 
238     endResetModel();
239 
240     bool brokenLinksAfter = hasBrokenLinks();
241     if (brokenLinksBefore != brokenLinksAfter)
242         emit hasBrokenLinksChanged(brokenLinksAfter);
243 }
244 
rowCount(const QModelIndex & parent) const245 int BrokenLinksModel::rowCount(const QModelIndex &parent) const
246 {
247     return parent.isValid() ? 0 : mBrokenLinks.count();
248 }
249 
columnCount(const QModelIndex & parent) const250 int BrokenLinksModel::columnCount(const QModelIndex &parent) const
251 {
252     return parent.isValid() ? 0 : 3; // file name | path | type
253 }
254 
data(const QModelIndex & index,int role) const255 QVariant BrokenLinksModel::data(const QModelIndex &index, int role) const
256 {
257     const BrokenLink &link = mBrokenLinks.at(index.row());
258 
259     switch (role) {
260     case Qt::DisplayRole:
261         switch (index.column()) {
262         case 0:
263             return QFileInfo(link.filePath()).fileName();
264         case 1:
265             return QFileInfo(link.filePath()).path();
266         case 2:
267             switch (link.type) {
268             case MapTilesetReference:
269                 return tr("Tileset");
270             case ObjectTemplateTilesetReference:
271                 return tr("Template tileset");
272             case TilesetImageSource:
273                 return tr("Tileset image");
274             case TilesetTileImageSource:
275                 return tr("Tile image");
276             case ObjectTemplateReference:
277                 return tr("Template");
278             }
279             break;
280         }
281         break;
282 
283     case Qt::DecorationRole:
284         switch (index.column()) {
285         case 0:
286             // todo: status icon
287             break;
288         }
289         break;
290     }
291 
292     return QVariant();
293 }
294 
headerData(int section,Qt::Orientation orientation,int role) const295 QVariant BrokenLinksModel::headerData(int section, Qt::Orientation orientation, int role) const
296 {
297     if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
298         switch (section) {
299         case 0: return tr("File name");
300         case 1: return tr("Location");
301         case 2: return tr("Type");
302         }
303     }
304     return QVariant();
305 }
306 
tileImageSourceChanged(Tile * tile)307 void BrokenLinksModel::tileImageSourceChanged(Tile *tile)
308 {
309     auto matchesTile = [tile](const BrokenLink &link) {
310         return link.type == TilesetTileImageSource && link._tile == tile;
311     };
312 
313     QVector<BrokenLink>::iterator it = std::find_if(mBrokenLinks.begin(),
314                                                     mBrokenLinks.end(),
315                                                     matchesTile);
316 
317     if (!tile->imageSource().isEmpty() && tile->imageStatus() == LoadingError) {
318         if (it != mBrokenLinks.end()) {
319             int linkIndex = it - mBrokenLinks.begin();
320             emit dataChanged(index(linkIndex, 0), index(linkIndex, 1));
321         } else {
322             refresh(); // lazy way of adding an entry for this tile
323         }
324     } else if (it != mBrokenLinks.end()) {
325         removeLink(it - mBrokenLinks.begin());
326     }
327 }
328 
tilesetChanged(Tileset * tileset)329 void BrokenLinksModel::tilesetChanged(Tileset *tileset)
330 {
331     Q_UNUSED(tileset)
332 
333     // This may mean either the tileset properties changed or tiles were
334     // added/removed from the tileset. Easiest to just refresh entirely.
335     refresh();
336 }
337 
tilesetAdded(int index,Tileset * tileset)338 void BrokenLinksModel::tilesetAdded(int index, Tileset *tileset)
339 {
340     Q_UNUSED(index)
341     connectToTileset(tileset->sharedPointer());
342     refresh();
343 }
344 
tilesetRemoved(Tileset * tileset)345 void BrokenLinksModel::tilesetRemoved(Tileset *tileset)
346 {
347     disconnectFromTileset(tileset->sharedPointer());
348     refresh();
349 }
350 
connectToTileset(const SharedTileset & tileset)351 void BrokenLinksModel::connectToTileset(const SharedTileset &tileset)
352 {
353     auto tilesetDocument = TilesetDocument::findDocumentForTileset(tileset);
354     if (tilesetDocument) {
355         connect(tilesetDocument, &TilesetDocument::tileImageSourceChanged,
356                 this, &BrokenLinksModel::tileImageSourceChanged);
357         connect(tilesetDocument, &TilesetDocument::tilesetChanged,
358                 this, &BrokenLinksModel::tilesetChanged);
359     }
360 }
361 
disconnectFromTileset(const SharedTileset & tileset)362 void BrokenLinksModel::disconnectFromTileset(const SharedTileset &tileset)
363 {
364     auto tilesetDocument = TilesetDocument::findDocumentForTileset(tileset);
365     if (tilesetDocument)
366         tilesetDocument->disconnect(this);
367 }
368 
removeLink(int index)369 void BrokenLinksModel::removeLink(int index)
370 {
371     beginRemoveRows(QModelIndex(), index, index);
372     mBrokenLinks.remove(index);
373     endRemoveRows();
374 
375     if (!hasBrokenLinks())
376         emit hasBrokenLinksChanged(false);
377 }
378 
379 
BrokenLinksWidget(BrokenLinksModel * brokenLinksModel,QWidget * parent)380 BrokenLinksWidget::BrokenLinksWidget(BrokenLinksModel *brokenLinksModel, QWidget *parent)
381     : QWidget(parent)
382     , mBrokenLinksModel(brokenLinksModel)
383     , mTitleLabel(new QLabel(this))
384     , mDescriptionLabel(new QLabel(this))
385     , mView(new QTreeView(this))
386     , mButtons(new QDialogButtonBox(QDialogButtonBox::Ignore,
387                                     Qt::Horizontal,
388                                     this))
389 {
390     mTitleLabel->setText(tr("Some files could not be found"));
391     mDescriptionLabel->setText(tr("One or more referenced files could not be found. You can help locate them below."));
392     mDescriptionLabel->setWordWrap(true);
393 
394     mLocateButton = mButtons->addButton(tr("Locate File..."), QDialogButtonBox::ActionRole);
395     mLocateButton->setEnabled(false);
396 
397     QFont font = mTitleLabel->font();
398     font.setBold(true);
399     mTitleLabel->setFont(font);
400 
401     mProxyModel = new QSortFilterProxyModel(this);
402     mProxyModel->setSortLocaleAware(true);
403     mProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
404     mProxyModel->setSourceModel(mBrokenLinksModel);
405 
406     mView->setModel(mProxyModel);
407     mView->setRootIsDecorated(false);
408     mView->setItemsExpandable(false);
409     mView->setUniformRowHeights(true);
410     mView->setSortingEnabled(true);
411     mView->sortByColumn(0, Qt::AscendingOrder);
412     mView->setSelectionMode(QAbstractItemView::ExtendedSelection);
413 
414     mView->header()->setStretchLastSection(false);
415     mView->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
416     mView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
417     mView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
418 
419     QBoxLayout *layout = new QVBoxLayout;
420     layout->addWidget(mTitleLabel);
421     layout->addWidget(mDescriptionLabel);
422     layout->addWidget(mView);
423     layout->addWidget(mButtons);
424     setLayout(layout);
425 
426     connect(mButtons, &QDialogButtonBox::clicked, this, &BrokenLinksWidget::clicked);
427 
428     connect(mView->selectionModel(), &QItemSelectionModel::selectionChanged,
429             this, &BrokenLinksWidget::selectionChanged);
430 
431     connect(mView, &QTreeView::doubleClicked, this, [this](const QModelIndex &proxyIndex) {
432         const auto index = mProxyModel->mapToSource(proxyIndex);
433         const BrokenLink &link = mBrokenLinksModel->brokenLink(index.row());
434         LinkFixer(mBrokenLinksModel->document()).tryFixLink(link);
435     });
436 
437     // For some reason a model reset doesn't trigger the selectionChanged signal,
438     // so we need to handle that explicitly.
439     connect(brokenLinksModel, &BrokenLinksModel::modelReset, this, &BrokenLinksWidget::selectionChanged);
440 }
441 
clicked(QAbstractButton * button)442 void BrokenLinksWidget::clicked(QAbstractButton *button)
443 {
444     if (button == mButtons->button(QDialogButtonBox::Ignore)) {
445         mBrokenLinksModel->document()->setIgnoreBrokenLinks(true);
446     } else if (button == mLocateButton) {
447         const auto proxySelection = mView->selectionModel()->selectedRows();
448         if (proxySelection.isEmpty())
449             return;
450 
451         QVector<BrokenLink> links;
452         links.reserve(proxySelection.size());
453 
454         for (const QModelIndex &proxyIndex : proxySelection) {
455             const auto index = mProxyModel->mapToSource(proxyIndex);
456             links.append(mBrokenLinksModel->brokenLink(index.row()));
457         }
458 
459         LinkFixer(mBrokenLinksModel->document()).tryFixLinks(links);
460     }
461 }
462 
selectionChanged()463 void BrokenLinksWidget::selectionChanged()
464 {
465     const auto selection = mView->selectionModel()->selectedRows();
466 
467     mLocateButton->setEnabled(!selection.isEmpty());
468 
469     bool isTileset = qobject_cast<TilesetDocument*>(mBrokenLinksModel->document()) != nullptr;
470 
471     if (!selection.isEmpty()) {
472         const auto firstIndex = selection.first();
473         const BrokenLink &link = mBrokenLinksModel->brokenLink(firstIndex.row());
474 
475         switch (link.type) {
476         case MapTilesetReference:
477         case ObjectTemplateReference:
478             mLocateButton->setText(tr("Locate File..."));
479             break;
480         case ObjectTemplateTilesetReference:
481             mLocateButton->setText(tr("Open Template..."));
482             break;
483         case TilesetTileImageSource:
484         case TilesetImageSource:
485             if (isTileset)
486                 mLocateButton->setText(tr("Locate File..."));
487             else
488                 mLocateButton->setText(tr("Open Tileset..."));
489             break;
490         }
491     }
492 }
493 
494 
LinkFixer(Document * document)495 LinkFixer::LinkFixer(Document *document)
496     : mDocument(document)
497 {
498 }
499 
tryFixLinks(const QVector<BrokenLink> & links)500 void LinkFixer::tryFixLinks(const QVector<BrokenLink> &links)
501 {
502     if (links.isEmpty())
503         return;
504 
505     if (links.size() == 1)
506         return tryFixLink(links.first());
507 
508     // If any of the links need to be fixed in a tileset, open the first such tileset and abort
509     bool editingTileset = mDocument->type() == Document::TilesetDocumentType;
510     for (const BrokenLink &link : links) {
511         if (link.type == TilesetImageSource || link.type == TilesetTileImageSource) {
512             if (!editingTileset) {
513                 // We need to open the tileset document in order to be able to make changes to it...
514                 const SharedTileset tileset = link.tileset()->sharedPointer();
515                 DocumentManager::instance()->openTileset(tileset);
516                 return;
517             }
518         }
519     }
520 
521     // todo: fix text on the button (says "Locate File")
522     static QString startingLocation = QFileInfo(links.first().filePath()).path();
523     const QString directory = QFileDialog::getExistingDirectory(MainWindow::instance(),
524                                                                 BrokenLinksWidget::tr("Locate Directory for Files"),
525                                                                 startingLocation);
526 
527     if (directory.isEmpty())
528         return;
529 
530     startingLocation = directory;
531 
532     const QDir dir(directory);
533     const auto entryList = dir.entryList(QDir::Files |
534                                          QDir::Readable |
535                                          QDir::NoDotAndDotDot);
536 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
537     const auto files = entryList.toSet();
538 #else
539     const QSet<QString> files { entryList.begin(), entryList.end() };
540 #endif
541 
542     // See if any of the links we're looking for is located in this directory
543     for (const BrokenLink &link : links) {
544         const QString fileName = QFileInfo(link.filePath()).fileName();
545         if (files.contains(fileName))
546             if (!tryFixLink(link, dir.filePath(fileName)))
547                 break;
548     }
549 
550     // todo: provide better feedback (like maybe a dialog showing any errors
551     // or the number of links fixed)
552 }
553 
tryFixLink(const BrokenLink & link)554 void LinkFixer::tryFixLink(const BrokenLink &link)
555 {
556     if (link.type == TilesetImageSource || link.type == TilesetTileImageSource) {
557         auto tilesetDocument = qobject_cast<TilesetDocument*>(mDocument);
558         if (!tilesetDocument) {
559             // We need to open the tileset document in order to be able to make changes to it...
560             const SharedTileset tileset = link.tileset()->sharedPointer();
561             DocumentManager::instance()->openTileset(tileset);
562             return;
563         }
564 
565         QUrl newFileUrl = locateImage(QFileInfo(link.filePath()).fileName());
566         if (newFileUrl.isEmpty())
567             return;
568 
569         // For local images, check if they can be loaded
570         if (newFileUrl.isLocalFile()) {
571             QString localFile = newFileUrl.toLocalFile();
572             tryFixLink(link, localFile);
573             return;
574         }
575 
576         if (link.type == TilesetImageSource) {
577             TilesetParameters parameters(*link._tileset);
578             parameters.imageSource = newFileUrl;
579 
580             auto command = new ChangeTilesetParameters(tilesetDocument,
581                                                        parameters);
582 
583             tilesetDocument->undoStack()->push(command);
584         } else {
585             auto command = new ChangeTileImageSource(tilesetDocument,
586                                                      link._tile,
587                                                      newFileUrl);
588 
589             tilesetDocument->undoStack()->push(command);
590         }
591 
592     } else if (link.type == ObjectTemplateTilesetReference) {
593         emit DocumentManager::instance()->templateOpenRequested(link.objectTemplate()->fileName());
594     } else if (link.type == MapTilesetReference) {
595         tryFixMapTilesetReference(link._tileset->sharedPointer());
596     } else if (link.type == ObjectTemplateReference) {
597         tryFixObjectTemplateReference(link.objectTemplate());
598     }
599 }
600 
tryFixLink(const BrokenLink & link,const QString & newFilePath)601 bool LinkFixer::tryFixLink(const BrokenLink &link, const QString &newFilePath)
602 {
603     Q_ASSERT(!newFilePath.isEmpty());
604 
605     if (link.type == TilesetImageSource || link.type == TilesetTileImageSource) {
606         auto tilesetDocument = qobject_cast<TilesetDocument*>(mDocument);
607         Q_ASSERT(tilesetDocument);
608 
609         QImageReader reader(newFilePath);
610         QImage image = reader.read();
611 
612         if (image.isNull()) {
613             QMessageBox::critical(MainWindow::instance(),
614                                   BrokenLinksWidget::tr("Error Loading Image"),
615                                   reader.errorString());
616             return false;
617         }
618 
619         const QUrl newSource(QUrl::fromLocalFile(newFilePath));
620 
621         if (link.type == TilesetImageSource) {
622             TilesetParameters parameters(*link._tileset);
623             parameters.imageSource = newSource;
624 
625             auto command = new ChangeTilesetParameters(tilesetDocument,
626                                                        parameters);
627 
628             tilesetDocument->undoStack()->push(command);
629         } else {
630             auto command = new ChangeTileImageSource(tilesetDocument,
631                                                      link._tile,
632                                                      newSource);
633 
634             tilesetDocument->undoStack()->push(command);
635         }
636 
637     } else if (link.type == MapTilesetReference) {
638         return tryFixMapTilesetReference(link._tileset->sharedPointer(), newFilePath);
639     } else if (link.type == ObjectTemplateReference) {
640         return tryFixObjectTemplateReference(link.objectTemplate(), newFilePath);
641     }
642 
643     return true;
644 }
645 
locateImage(const QString & fileName)646 QUrl LinkFixer::locateImage(const QString &fileName)
647 {
648     Session &session = Session::current();
649     QString startLocation = session.lastPath(Session::ImageFile);
650     startLocation += QLatin1Char('/');
651     startLocation += fileName;
652 
653     QUrl newFileUrl = QFileDialog::getOpenFileUrl(MainWindow::instance(),
654                                                   BrokenLinksWidget::tr("Locate File"),
655                                                   QUrl::fromLocalFile(startLocation),
656                                                   Utils::readableImageFormatsFilter());
657 
658     if (newFileUrl.isLocalFile()) {
659         QString localFile = newFileUrl.toLocalFile();
660         session.setLastPath(Session::ImageFile, QFileInfo(localFile).absolutePath());
661     }
662 
663     return newFileUrl;
664 }
665 
locateTileset()666 QString LinkFixer::locateTileset()
667 {
668     FormatHelper<TilesetFormat> helper(FileFormat::Read, BrokenLinksWidget::tr("All Files (*)"));
669 
670     Session &session = Session::current();
671     QString start = session.lastPath(Session::ExternalTileset);
672     QString fileName = QFileDialog::getOpenFileName(MainWindow::instance(),
673                                                     BrokenLinksWidget::tr("Locate External Tileset"),
674                                                     start,
675                                                     helper.filter());
676 
677     if (!fileName.isEmpty())
678         session.setLastPath(Session::ExternalTileset, QFileInfo(fileName).path());
679 
680     return fileName;
681 }
682 
locateObjectTemplate()683 QString LinkFixer::locateObjectTemplate()
684 {
685     FormatHelper<ObjectTemplateFormat> helper(FileFormat::Read, BrokenLinksWidget::tr("All Files (*)"));
686 
687     Session &session = Session::current();
688     QString start = session.lastPath(Session::ObjectTemplateFile);
689     QString fileName = QFileDialog::getOpenFileName(MainWindow::instance(),
690                                                     BrokenLinksWidget::tr("Locate Object Template"),
691                                                     start,
692                                                     helper.filter());
693 
694     if (!fileName.isEmpty())
695         session.setLastPath(Session::ObjectTemplateFile, QFileInfo(fileName).path());
696 
697     return fileName;
698 }
699 
tryFixMapTilesetReference(const SharedTileset & tileset)700 void LinkFixer::tryFixMapTilesetReference(const SharedTileset &tileset)
701 {
702     QString fileName = locateTileset();
703     if (!fileName.isEmpty())
704         tryFixMapTilesetReference(tileset, fileName);
705 }
706 
tryFixObjectTemplateReference(const ObjectTemplate * objectTemplate)707 void LinkFixer::tryFixObjectTemplateReference(const ObjectTemplate *objectTemplate)
708 {
709     QString fileName = locateObjectTemplate();
710     if (!fileName.isEmpty())
711         tryFixObjectTemplateReference(objectTemplate, fileName);
712 }
713 
tryFixMapTilesetReference(const SharedTileset & tileset,const QString & newFilePath)714 bool LinkFixer::tryFixMapTilesetReference(const SharedTileset &tileset, const QString &newFilePath)
715 {
716     // It could be, that we have already loaded this tileset.
717     SharedTileset newTileset = TilesetManager::instance()->findTileset(newFilePath);
718     if (!newTileset || newTileset->status() == LoadingError) {
719         QString error;
720         newTileset = readTileset(newFilePath, &error);
721 
722         if (!newTileset) {
723             QMessageBox::critical(MainWindow::instance(), BrokenLinksWidget::tr("Error Reading Tileset"), error);
724             return false;
725         }
726     }
727 
728     MapDocument *mapDocument = static_cast<MapDocument*>(mDocument);
729     int index = mapDocument->map()->tilesets().indexOf(tileset);
730     if (index != -1) {
731         mDocument->undoStack()->push(new ReplaceTileset(mapDocument, index, newTileset));
732         return true;
733     }
734 
735     return false;
736 }
737 
tryFixObjectTemplateReference(const ObjectTemplate * objectTemplate,const QString & newFilePath)738 bool LinkFixer::tryFixObjectTemplateReference(const ObjectTemplate *objectTemplate, const QString &newFilePath)
739 {
740     ObjectTemplate *newObjectTemplate = TemplateManager::instance()->findObjectTemplate(newFilePath);
741 
742     if (!newObjectTemplate || !newObjectTemplate->object()) {
743         QString error;
744         newObjectTemplate = TemplateManager::instance()->loadObjectTemplate(newFilePath, &error);
745 
746         if (!newObjectTemplate->object()) {
747             QMessageBox::critical(MainWindow::instance(), BrokenLinksWidget::tr("Error Reading Object Template"), error);
748             return false;
749         }
750     }
751 
752     MapDocument *mapDocument = static_cast<MapDocument*>(mDocument);
753     mDocument->undoStack()->push(new ReplaceTemplate(mapDocument,
754                                                      objectTemplate,
755                                                      newObjectTemplate));
756     return true;
757 }
758 
759 
operator ()() const760 void LocateTileset::operator ()() const
761 {
762     SharedTileset tileset = mTileset.lock();
763     MapDocumentPtr mapDocument = mMapDocument.lock();
764     if (!tileset || !mapDocument)
765         return;
766 
767     LinkFixer(mapDocument.data()).tryFixMapTilesetReference(tileset);
768 }
769 
operator ()() const770 void LocateObjectTemplate::operator()() const
771 {
772     MapDocumentPtr mapDocument = mMapDocument.lock();
773     if (!mapDocument)
774         return;
775 
776     LinkFixer(mapDocument.data()).tryFixObjectTemplateReference(mObjectTemplate);
777 }
778 
779 } // namespace Tiled
780 
781 #include "moc_brokenlinks.cpp"
782