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