1 /*
2  * documentmanager.cpp
3  * Copyright 2010, Stefan Beller <stefanbeller@googlemail.com>
4  * Copyright 2010-2016, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
5  *
6  * This file is part of Tiled.
7  *
8  * This program is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU General Public License as published by the Free
10  * Software Foundation; either version 2 of the License, or (at your option)
11  * any later version.
12  *
13  * This program is distributed in the hope that it will be useful, but WITHOUT
14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16  * more details.
17  *
18  * You should have received a copy of the GNU General Public License along with
19  * this program. If not, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include "documentmanager.h"
23 
24 #include "abstracttool.h"
25 #include "adjusttileindexes.h"
26 #include "brokenlinks.h"
27 #include "containerhelpers.h"
28 #include "editableasset.h"
29 #include "editor.h"
30 #include "filechangedwarning.h"
31 #include "filesystemwatcher.h"
32 #include "logginginterface.h"
33 #include "map.h"
34 #include "mapdocument.h"
35 #include "mapeditor.h"
36 #include "mapformat.h"
37 #include "maprenderer.h"
38 #include "mapview.h"
39 #include "noeditorwidget.h"
40 #include "preferences.h"
41 #include "projectmanager.h"
42 #include "session.h"
43 #include "tabbar.h"
44 #include "tilesetdocument.h"
45 #include "tilesetdocumentsmodel.h"
46 #include "tilesetmanager.h"
47 #include "tmxmapformat.h"
48 #include "utils.h"
49 #include "wangset.h"
50 #include "worlddocument.h"
51 #include "worldmanager.h"
52 #include "zoomable.h"
53 
54 #include <QCoreApplication>
55 #include <QDialogButtonBox>
56 #include <QFileDialog>
57 #include <QFileInfo>
58 #include <QHBoxLayout>
59 #include <QLabel>
60 #include <QMenu>
61 #include <QMessageBox>
62 #include <QScrollBar>
63 #include <QStackedLayout>
64 #include <QTabBar>
65 #include <QTabWidget>
66 #include <QUndoGroup>
67 #include <QUndoStack>
68 #include <QVBoxLayout>
69 
70 #include "qtcompat_p.h"
71 
72 using namespace Tiled;
73 
74 
75 DocumentManager *DocumentManager::mInstance;
76 
instance()77 DocumentManager *DocumentManager::instance()
78 {
79     Q_ASSERT(mInstance);
80     return mInstance;
81 }
82 
maybeInstance()83 DocumentManager *DocumentManager::maybeInstance()
84 {
85     return mInstance;
86 }
87 
DocumentManager(QObject * parent)88 DocumentManager::DocumentManager(QObject *parent)
89     : QObject(parent)
90     , mTilesetDocumentsModel(new TilesetDocumentsModel(this))
91     , mWidget(new QWidget)
92     , mNoEditorWidget(new NoEditorWidget(mWidget))
93     , mTabBar(new TabBar(mWidget))
94     , mFileChangedWarning(new FileChangedWarning(mWidget))
95     , mBrokenLinksModel(new BrokenLinksModel(this))
96     , mBrokenLinksWidget(new BrokenLinksWidget(mBrokenLinksModel, mWidget))
97     , mMapEditor(nullptr) // todo: look into removing this
98     , mUndoGroup(new QUndoGroup(this))
99     , mFileSystemWatcher(new FileSystemWatcher(this))
100     , mMultiDocumentClose(false)
101 {
102     Q_ASSERT(!mInstance);
103     mInstance = this;
104 
105     mBrokenLinksWidget->setVisible(false);
106 
107     mTabBar->setExpanding(false);
108     mTabBar->setDocumentMode(true);
109     mTabBar->setTabsClosable(true);
110     mTabBar->setMovable(true);
111     mTabBar->setContextMenuPolicy(Qt::CustomContextMenu);
112 
113     mFileChangedWarning->setVisible(false);
114 
115     connect(mFileChangedWarning, &FileChangedWarning::reload, this, &DocumentManager::reloadCurrentDocument);
116     connect(mFileChangedWarning, &FileChangedWarning::ignore, this, &DocumentManager::hideChangedWarning);
117 
118     QVBoxLayout *vertical = new QVBoxLayout(mWidget);
119     vertical->addWidget(mTabBar);
120     vertical->addWidget(mFileChangedWarning);
121     vertical->addWidget(mBrokenLinksWidget);
122     vertical->setContentsMargins(0, 0, 0, 0);
123     vertical->setSpacing(0);
124 
125     mEditorStack = new QStackedLayout;
126     mEditorStack->addWidget(mNoEditorWidget);
127     vertical->addLayout(mEditorStack);
128 
129     connect(mTabBar, &QTabBar::currentChanged,
130             this, &DocumentManager::currentIndexChanged);
131     connect(mTabBar, &QTabBar::tabCloseRequested,
132             this, &DocumentManager::documentCloseRequested);
133     connect(mTabBar, &QTabBar::tabMoved,
134             this, &DocumentManager::documentTabMoved);
135     connect(mTabBar, &QWidget::customContextMenuRequested,
136             this, &DocumentManager::tabContextMenuRequested);
137 
138     connect(mFileSystemWatcher, &FileSystemWatcher::pathsChanged,
139             this, &DocumentManager::filesChanged);
140 
141     connect(mBrokenLinksModel, &BrokenLinksModel::hasBrokenLinksChanged,
142             mBrokenLinksWidget, &BrokenLinksWidget::setVisible);
143 
144     connect(TilesetManager::instance(), &TilesetManager::tilesetImagesChanged,
145             this, &DocumentManager::tilesetImagesChanged);
146 
147     connect(Preferences::instance(), &Preferences::aboutToSwitchSession,
148             this, &DocumentManager::updateSession);
149 
150     OpenFile::activated = [this] (const OpenFile &open) {
151         openFile(open.file);
152     };
153 
154     JumpToTile::activated = [this] (const JumpToTile &jump) {
155         if (auto mapDocument = openMapFile(jump.mapFile)) {
156             auto renderer = mapDocument->renderer();
157             auto mapView = viewForDocument(mapDocument);
158             auto pos = renderer->tileToScreenCoords(jump.tilePos);
159 
160             if (auto layer = mapDocument->map()->findLayerById(jump.layerId)) {
161                 mapDocument->switchSelectedLayers({ layer });
162                 mapView->forceCenterOn(pos, *layer);
163             } else {
164                 mapView->forceCenterOn(pos);
165             }
166         }
167     };
168 
169     JumpToObject::activated = [this] (const JumpToObject &jump) {
170         if (auto mapDocument = openMapFile(jump.mapFile)) {
171             if (auto object = mapDocument->map()->findObjectById(jump.objectId)) {
172                 mapDocument->focusMapObjectRequested(object);
173                 mapDocument->setSelectedObjects({ object });
174             }
175         }
176     };
177 
178     SelectLayer::activated = [this] (const SelectLayer &select) {
179         if (auto mapDocument = openMapFile(select.mapFile)) {
180             if (auto layer = mapDocument->map()->findLayerById(select.layerId)) {
181                 mapDocument->switchSelectedLayers({ layer });
182                 mapDocument->setCurrentObject(layer);
183             }
184         }
185     };
186 
187     SelectCustomProperty::activated = [this] (const SelectCustomProperty &select) {
188         openFile(select.fileName);
189         const int i = findDocument(select.fileName);
190         if (i == -1)
191             return;
192 
193         auto doc = mDocuments.at(i).data();
194         Object *obj = nullptr;
195 
196         switch (doc->type()) {
197         case Document::MapDocumentType: {
198             auto mapDocument = static_cast<MapDocument*>(doc);
199             switch (select.objectType) {
200             case Object::LayerType:
201                 if (auto layer = mapDocument->map()->findLayerById(select.id)) {
202                     mapDocument->switchSelectedLayers({ layer });
203                     obj = layer;
204                 }
205                 break;
206             case Object::MapObjectType:
207                 if (auto object = mapDocument->map()->findObjectById(select.id)) {
208                     mapDocument->focusMapObjectRequested(object);
209                     mapDocument->setSelectedObjects({ object });
210                     obj = object;
211                 }
212                 break;
213             case Object::MapType:
214                 obj = mapDocument->map();
215                 break;
216             case Object::ObjectTemplateType:
217                 emit templateOpenRequested(select.fileName);
218                 // todo: can't access Object pointer
219                 break;
220             }
221             break;
222         }
223         case Document::TilesetDocumentType: {
224             auto tilesetDocument = static_cast<TilesetDocument*>(doc);
225             switch (select.objectType) {
226             case Object::MapObjectType:
227                 // todo: no way to know to which tile this object belongs
228                 break;
229             case Object::TilesetType:
230                 obj = tilesetDocument->tileset().data();
231                 break;
232             case Object::TileType:
233                 if (auto tile = tilesetDocument->tileset()->findTile(select.id)) {
234                     tilesetDocument->setSelectedTiles({ tile });
235                     obj = tile;
236                 }
237                 break;
238             case Object::WangSetType: {
239                 // todo: select the wang set
240                 if (select.id < tilesetDocument->tileset()->wangSetCount())
241                     obj = tilesetDocument->tileset()->wangSet(select.id);
242                 break;
243             }
244             case Object::WangColorType:
245                 // todo: can't select just by color index
246                 break;
247             case Object::ObjectTemplateType:
248                 emit templateOpenRequested(select.fileName);
249                 // todo: can't access Object pointer
250                 break;
251             }
252             break;
253         }
254         case Document::WorldDocumentType:
255             break;
256         }
257 
258         if (obj) {
259             doc->setCurrentObject(obj);
260             emit selectCustomPropertyRequested(select.propertyName);
261         }
262     };
263 
264     SelectTile::activated = [this] (const SelectTile &select) {
265         TilesetDocument* tilesetDocument = nullptr;
266 
267         if (SharedTileset tileset { select.tileset }) {
268             tilesetDocument = findTilesetDocument(tileset);
269             if (tilesetDocument) {
270                 if (!switchToDocument(tilesetDocument))
271                     addDocument(tilesetDocument->sharedFromThis());
272             }
273         }
274 
275         if (!tilesetDocument && !select.tilesetFile.isEmpty())
276             tilesetDocument = openTilesetFile(select.tilesetFile);
277 
278         if (tilesetDocument) {
279             if (auto tile = tilesetDocument->tileset()->findTile(select.tileId)) {
280                 tilesetDocument->setSelectedTiles({ tile });
281                 tilesetDocument->setCurrentObject(tile);
282             }
283         }
284     };
285 
286     WorldManager& worldManager = WorldManager::instance();
287     connect(&worldManager, &WorldManager::worldUnloaded,
288             this, &DocumentManager::onWorldUnloaded);
289 }
290 
~DocumentManager()291 DocumentManager::~DocumentManager()
292 {
293     // All documents should be closed gracefully beforehand
294     Q_ASSERT(mDocuments.isEmpty());
295     Q_ASSERT(mTilesetDocumentsModel->rowCount() == 0);
296     delete mWidget;
297 
298     mInstance = nullptr;
299 }
300 
301 /**
302  * Returns the document manager widget. It contains the different map views
303  * and a tab bar to switch between them.
304  */
widget() const305 QWidget *DocumentManager::widget() const
306 {
307     return mWidget;
308 }
309 
setEditor(Document::DocumentType documentType,Editor * editor)310 void DocumentManager::setEditor(Document::DocumentType documentType, Editor *editor)
311 {
312     Q_ASSERT(!mEditorForType.contains(documentType));
313     mEditorForType.insert(documentType, editor);
314     mEditorStack->addWidget(editor->editorWidget());
315 
316     if (MapEditor *mapEditor = qobject_cast<MapEditor*>(editor))
317         mMapEditor = mapEditor;
318 }
319 
editor(Document::DocumentType documentType) const320 Editor *DocumentManager::editor(Document::DocumentType documentType) const
321 {
322     return mEditorForType.value(documentType);
323 }
324 
deleteEditors()325 void DocumentManager::deleteEditors()
326 {
327     qDeleteAll(mEditorForType);
328     mEditorForType.clear();
329     mMapEditor = nullptr;
330 }
331 
editors() const332 QList<Editor *> DocumentManager::editors() const
333 {
334     return mEditorForType.values();
335 }
336 
currentEditor() const337 Editor *DocumentManager::currentEditor() const
338 {
339     if (const auto document = currentDocument())
340         return editor(document->type());
341 
342     return nullptr;
343 }
344 
saveState()345 void DocumentManager::saveState()
346 {
347     QHashIterator<Document::DocumentType, Editor*> iterator(mEditorForType);
348     while (iterator.hasNext())
349         iterator.next().value()->saveState();
350 }
351 
restoreState()352 void DocumentManager::restoreState()
353 {
354     QHashIterator<Document::DocumentType, Editor*> iterator(mEditorForType);
355     while (iterator.hasNext())
356         iterator.next().value()->restoreState();
357 }
358 
359 /**
360  * Returns the current map document, or 0 when there is none.
361  */
currentDocument() const362 Document *DocumentManager::currentDocument() const
363 {
364     const int index = mTabBar->currentIndex();
365     if (index == -1)
366         return nullptr;
367 
368     return mDocuments.at(index).data();
369 }
370 
371 /**
372  * Returns the map view of the current document, or 0 when there is none.
373  */
currentMapView() const374 MapView *DocumentManager::currentMapView() const
375 {
376     return mMapEditor->currentMapView();
377 }
378 
379 /**
380  * Returns the map view that displays the given document, or null when there
381  * is none.
382  */
viewForDocument(MapDocument * mapDocument) const383 MapView *DocumentManager::viewForDocument(MapDocument *mapDocument) const
384 {
385     return mMapEditor->viewForDocument(mapDocument);
386 }
387 
388 /**
389  * Searches for a document with the given \a fileName and returns its
390  * index. Returns -1 when the document isn't open.
391  */
findDocument(const QString & fileName) const392 int DocumentManager::findDocument(const QString &fileName) const
393 {
394     const QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
395     if (canonicalFilePath.isEmpty()) // file doesn't exist
396         return -1;
397 
398     for (int i = 0; i < mDocuments.size(); ++i) {
399         if (mDocuments.at(i)->canonicalFilePath() == canonicalFilePath)
400             return i;
401     }
402 
403     return -1;
404 }
405 
findDocument(Document * document) const406 int DocumentManager::findDocument(Document *document) const
407 {
408     return indexOf(mDocuments, document);
409 }
410 
411 /**
412  * Switches to the map document at the given \a index.
413  */
switchToDocument(int index)414 void DocumentManager::switchToDocument(int index)
415 {
416     mTabBar->setCurrentIndex(index);
417 }
418 
switchToDocument(const QString & fileName)419 bool DocumentManager::switchToDocument(const QString &fileName)
420 {
421     const int index = findDocument(fileName);
422     if (index != -1) {
423         switchToDocument(index);
424         return true;
425     }
426     return false;
427 }
428 
429 /**
430  * Switches to the given \a document, if there is already a tab open for it.
431  * \return whether the switch was succesful
432  */
switchToDocument(Document * document)433 bool DocumentManager::switchToDocument(Document *document)
434 {
435     const int index = findDocument(document);
436     if (index != -1) {
437         switchToDocument(index);
438         return true;
439     }
440     return false;
441 }
442 
443 /**
444  * Switches to the given \a mapDocument, centering the view on \a viewCenter
445  * (scene coordinates) at the given \a scale.
446  *
447  * If the given map document is not open yet, a tab will be created for it.
448  */
switchToDocument(MapDocument * mapDocument,QPointF viewCenter,qreal scale)449 void DocumentManager::switchToDocument(MapDocument *mapDocument, QPointF viewCenter, qreal scale)
450 {
451     if (!switchToDocument(mapDocument))
452         addDocument(mapDocument->sharedFromThis());
453 
454     MapView *view = currentMapView();
455     view->zoomable()->setScale(scale);
456     view->forceCenterOn(viewCenter);
457 }
458 
459 /**
460  * Switches to the given \a mapDocument, taking tilesets into accout
461  */
switchToDocumentAndHandleSimiliarTileset(MapDocument * mapDocument,QPointF viewCenter,qreal scale)462 void DocumentManager::switchToDocumentAndHandleSimiliarTileset(MapDocument *mapDocument, QPointF viewCenter, qreal scale)
463 {
464     // Try selecting similar layers and tileset by name to the previously active mapitem
465     SharedTileset newSimilarTileset;
466 
467     if (auto currentMapDocument = qobject_cast<MapDocument*>(currentDocument())) {
468         const Layer *currentLayer = currentMapDocument->currentLayer();
469         const QList<Layer*> selectedLayers = currentMapDocument->selectedLayers();
470 
471         if (currentLayer) {
472             Layer *newCurrentLayer = mapDocument->map()->findLayer(currentLayer->name(),
473                                                                    currentLayer->layerType());
474             if (newCurrentLayer)
475                 mapDocument->setCurrentLayer(newCurrentLayer);
476         }
477 
478         QList<Layer*> newSelectedLayers;
479         for (Layer *selectedLayer : selectedLayers) {
480             Layer *newSelectedLayer = mapDocument->map()->findLayer(selectedLayer->name(),
481                                                                     selectedLayer->layerType());
482             if (newSelectedLayer)
483                 newSelectedLayers << newSelectedLayer;
484         }
485         if (!newSelectedLayers.isEmpty())
486             mapDocument->setSelectedLayers(newSelectedLayers);
487 
488         Editor *currentEditor = DocumentManager::instance()->currentEditor();
489         if (auto currentMapEditor = qobject_cast<MapEditor*>(currentEditor)) {
490             if (SharedTileset currentTileset = currentMapEditor->currentTileset()) {
491                 if (!mapDocument->map()->tilesets().contains(currentTileset))
492                     newSimilarTileset = currentTileset->findSimilarTileset(mapDocument->map()->tilesets());
493             }
494         }
495     }
496 
497     DocumentManager::instance()->switchToDocument(mapDocument, viewCenter, scale);
498 
499     Editor *newEditor = DocumentManager::instance()->currentEditor();
500     if (auto newMapEditor = qobject_cast<MapEditor*>(newEditor))
501         if (newSimilarTileset)
502             newMapEditor->setCurrentTileset(newSimilarTileset);
503 }
504 
switchToLeftDocument()505 void DocumentManager::switchToLeftDocument()
506 {
507     const int tabCount = mTabBar->count();
508     if (tabCount < 2)
509         return;
510 
511     const int currentIndex = mTabBar->currentIndex();
512     switchToDocument((currentIndex > 0 ? currentIndex : tabCount) - 1);
513 }
514 
switchToRightDocument()515 void DocumentManager::switchToRightDocument()
516 {
517     const int tabCount = mTabBar->count();
518     if (tabCount < 2)
519         return;
520 
521     const int currentIndex = mTabBar->currentIndex();
522     switchToDocument((currentIndex + 1) % tabCount);
523 }
524 
openFileDialog()525 void DocumentManager::openFileDialog()
526 {
527     emit fileOpenDialogRequested();
528 }
529 
openFile(const QString & path)530 void DocumentManager::openFile(const QString &path)
531 {
532     emit fileOpenRequested(path);
533 }
534 
saveFile()535 void DocumentManager::saveFile()
536 {
537     emit fileSaveRequested();
538 }
539 
540 /**
541  * Adds the new or opened \a document to the document manager.
542  */
addDocument(const DocumentPtr & document)543 void DocumentManager::addDocument(const DocumentPtr &document)
544 {
545     insertDocument(mDocuments.size(), document);
546 }
547 
insertDocument(int index,const DocumentPtr & document)548 void DocumentManager::insertDocument(int index, const DocumentPtr &document)
549 {
550     Q_ASSERT(document);
551     Q_ASSERT(!mDocuments.contains(document));
552 
553     mDocuments.insert(index, document);
554     mUndoGroup->addStack(document->undoStack());
555 
556     Document *documentPtr = document.data();
557 
558     if (auto mapDocument = qobject_cast<MapDocument*>(documentPtr)) {
559         for (const SharedTileset &tileset : mapDocument->map()->tilesets())
560             addToTilesetDocument(tileset, mapDocument);
561     } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(documentPtr)) {
562         // We may have opened a bare tileset that wasn't seen before
563         if (!mTilesetDocumentsModel->contains(tilesetDocument)) {
564             mTilesetDocumentsModel->append(tilesetDocument);
565             emit tilesetDocumentAdded(tilesetDocument);
566         }
567     }
568 
569     if (!document->fileName().isEmpty())
570         mFileSystemWatcher->addPath(document->fileName());
571 
572     if (Editor *editor = mEditorForType.value(document->type()))
573         editor->addDocument(documentPtr);
574 
575     QString tabText = document->displayName();
576     if (document->isModified())
577         tabText.prepend(QLatin1Char('*'));
578 
579     const int documentIndex = mTabBar->insertTab(index, tabText);
580     mTabBar->setTabToolTip(documentIndex, document->fileName());
581 
582     connect(documentPtr, &Document::fileNameChanged, this, &DocumentManager::fileNameChanged);
583     connect(documentPtr, &Document::modifiedChanged, this, [=] { updateDocumentTab(documentPtr); });
584     connect(documentPtr, &Document::saved, this, &DocumentManager::onDocumentSaved);
585 
586     if (auto *mapDocument = qobject_cast<MapDocument*>(documentPtr)) {
587         connect(mapDocument, &MapDocument::tilesetAdded, this, &DocumentManager::tilesetAdded);
588         connect(mapDocument, &MapDocument::tilesetRemoved, this, &DocumentManager::tilesetRemoved);
589     }
590 
591     if (auto *tilesetDocument = qobject_cast<TilesetDocument*>(documentPtr))
592         connect(tilesetDocument, &TilesetDocument::tilesetNameChanged, this, &DocumentManager::tilesetNameChanged);
593 
594     switchToDocument(documentIndex);
595 
596     if (mBrokenLinksModel->hasBrokenLinks())
597         mBrokenLinksWidget->show();
598 
599     emit documentOpened(documentPtr);
600 }
601 
602 /**
603  * Returns whether the given document has unsaved modifications. For map files
604  * with embedded tilesets, that includes checking whether any of the embedded
605  * tilesets have unsaved modifications.
606  */
isDocumentModified(Document * document) const607 bool DocumentManager::isDocumentModified(Document *document) const
608 {
609     if (auto mapDocument = qobject_cast<MapDocument*>(document)) {
610         for (const SharedTileset &tileset : mapDocument->map()->tilesets()) {
611             if (const auto tilesetDocument = findTilesetDocument(tileset))
612                 if (tilesetDocument->isEmbedded() && tilesetDocument->isModified())
613                     return true;
614         }
615     }
616 
617     return document->isModified();
618 }
619 
620 /**
621  * Returns whether the given document was changed on disk. Taking into account
622  * the case where the given document is an embedded tileset document.
623  */
isDocumentChangedOnDisk(Document * document)624 static bool isDocumentChangedOnDisk(Document *document)
625 {
626     if (auto tilesetDocument = qobject_cast<TilesetDocument*>(document)) {
627         if (tilesetDocument->isEmbedded())
628             document = tilesetDocument->mapDocuments().first();
629     }
630 
631     return document->changedOnDisk();
632 }
633 
loadDocument(const QString & fileName,FileFormat * fileFormat,QString * error)634 DocumentPtr DocumentManager::loadDocument(const QString &fileName,
635                                           FileFormat *fileFormat,
636                                           QString *error)
637 {
638     // Try to find it in already loaded documents
639     QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
640     if (Document *doc = Document::documentInstances().value(canonicalFilePath))
641         return doc->sharedFromThis();
642 
643     if (!fileFormat) {
644         // Try to find a plugin that implements support for this format
645         fileFormat = PluginManager::find<FileFormat>([&](FileFormat *format) {
646             return format->hasCapabilities(FileFormat::Read) && format->supportsFile(fileName);
647         });
648     }
649 
650     if (!fileFormat) {
651         if (error)
652             *error = tr("Unrecognized file format.");
653         return DocumentPtr();
654     }
655 
656     DocumentPtr document;
657 
658     if (MapFormat *mapFormat = qobject_cast<MapFormat*>(fileFormat)) {
659         document = MapDocument::load(fileName, mapFormat, error);
660     } else if (TilesetFormat *tilesetFormat = qobject_cast<TilesetFormat*>(fileFormat)) {
661         // It could be, that we have already loaded this tileset while loading some map.
662         if (auto tilesetDocument = findTilesetDocument(fileName)) {
663             document = tilesetDocument->sharedFromThis();
664         } else {
665             document = TilesetDocument::load(fileName, tilesetFormat, error);
666         }
667     }
668 
669     return document;
670 }
671 
672 /**
673  * Save the given document with the given file name.
674  *
675  * @return <code>true</code> on success, <code>false</code> on failure
676  */
saveDocument(Document * document,const QString & fileName)677 bool DocumentManager::saveDocument(Document *document, const QString &fileName)
678 {
679     if (fileName.isEmpty())
680         return false;
681 
682     emit documentAboutToBeSaved(document);
683 
684     QString error;
685     if (!document->save(fileName, &error)) {
686         switchToDocument(document);
687         QMessageBox::critical(mWidget->window(), QCoreApplication::translate("Tiled::MainWindow", "Error Saving File"), error);
688         return false;
689     }
690 
691     emit documentSaved(document);
692 
693     return true;
694 }
695 
696 /**
697  * Save the given document with a file name chosen by the user. When saved
698  * successfully, the file is added to the list of recent files.
699  *
700  * @return <code>true</code> on success, <code>false</code> on failure
701  */
saveDocumentAs(Document * document)702 bool DocumentManager::saveDocumentAs(Document *document)
703 {
704     QString selectedFilter;
705     QString fileName = document->fileName();
706 
707     if (FileFormat *format = document->writerFormat())
708         selectedFilter = format->nameFilter();
709 
710     auto getSaveFileName = [&](const QString &filter, const QString &defaultFileName) {
711         if (fileName.isEmpty()) {
712             fileName = fileDialogStartLocation();
713             fileName += QLatin1Char('/');
714             fileName += defaultFileName;
715             fileName += Utils::firstExtension(selectedFilter);
716         }
717 
718         while (true) {
719             fileName = QFileDialog::getSaveFileName(mWidget->window(), tr("Save File As"),
720                                                     fileName,
721                                                     filter,
722                                                     &selectedFilter);
723 
724             if (!fileName.isEmpty() &&
725                 !Utils::fileNameMatchesNameFilter(fileName, selectedFilter))
726             {
727                 QMessageBox messageBox(QMessageBox::Warning,
728                                        QCoreApplication::translate("Tiled::MainWindow", "Extension Mismatch"),
729                                        QCoreApplication::translate("Tiled::MainWindow", "The file extension does not match the chosen file type."),
730                                        QMessageBox::Yes | QMessageBox::No,
731                                        mWidget->window());
732 
733                 messageBox.setInformativeText(QCoreApplication::translate("Tiled::MainWindow",
734                                                                           "Tiled may not automatically recognize your file when loading. "
735                                                                           "Are you sure you want to save with this extension?"));
736 
737                 int answer = messageBox.exec();
738                 if (answer != QMessageBox::Yes)
739                     continue;
740             }
741             return fileName;
742         }
743     };
744 
745     if (auto mapDocument = qobject_cast<MapDocument*>(document)) {
746         FormatHelper<MapFormat> helper(FileFormat::ReadWrite);
747         SessionOption<QString> lastUsedMapFormat { "map.lastUsedFormat" };
748 
749         if (selectedFilter.isEmpty()) {
750             if (auto format = helper.findFormat(lastUsedMapFormat))
751                 selectedFilter = format->nameFilter();
752         }
753 
754         if (selectedFilter.isEmpty())
755             selectedFilter = TmxMapFormat().nameFilter();
756 
757         auto suggestedFileName = QCoreApplication::translate("Tiled::MainWindow", "untitled");
758 
759         fileName = getSaveFileName(helper.filter(), suggestedFileName);
760         if (fileName.isEmpty())
761             return false;
762 
763         MapFormat *format = helper.formatByNameFilter(selectedFilter);
764         mapDocument->setWriterFormat(format);
765         mapDocument->setReaderFormat(format);
766 
767         lastUsedMapFormat = format->shortName();
768 
769     } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(document)) {
770         FormatHelper<TilesetFormat> helper(FileFormat::ReadWrite);
771         SessionOption<QString> lastUsedTilesetFormat { "tileset.lastUsedFormat" };
772 
773         if (selectedFilter.isEmpty()) {
774             if (auto format = helper.findFormat(lastUsedTilesetFormat))
775                 selectedFilter = format->nameFilter();
776         }
777 
778         if (selectedFilter.isEmpty())
779             selectedFilter = TsxTilesetFormat().nameFilter();
780 
781         auto suggestedFileName = tilesetDocument->tileset()->name().trimmed();
782         if (suggestedFileName.isEmpty())
783             suggestedFileName = QCoreApplication::translate("Tiled::MainWindow", "untitled");
784 
785         fileName = getSaveFileName(helper.filter(), suggestedFileName);
786         if (fileName.isEmpty())
787             return false;
788 
789         TilesetFormat *format = helper.formatByNameFilter(selectedFilter);
790         tilesetDocument->setWriterFormat(format);
791 
792         lastUsedTilesetFormat = format->shortName();
793     }
794 
795     return saveDocument(document, fileName);
796 }
797 
798 /**
799  * Closes the current map document. Will not ask the user whether to save
800  * any changes!
801  */
closeCurrentDocument()802 void DocumentManager::closeCurrentDocument()
803 {
804     const int index = mTabBar->currentIndex();
805     if (index == -1)
806         return;
807 
808     closeDocumentAt(index);
809 }
810 
811 /**
812  * Close all documents. Will not ask the user whether to save any changes!
813  */
closeAllDocuments()814 void DocumentManager::closeAllDocuments()
815 {
816     while (!mDocuments.isEmpty())
817         closeCurrentDocument();
818 }
819 
820 /**
821  * Closes all documents except the one pointed to by index.
822  */
closeOtherDocuments(int index)823 void DocumentManager::closeOtherDocuments(int index)
824 {
825     if (index == -1)
826         return;
827 
828     mMultiDocumentClose = true;
829 
830     for (int i = mTabBar->count() - 1; i >= 0; --i) {
831         if (i != index)
832             documentCloseRequested(i);
833 
834         if (!mMultiDocumentClose)
835             return;
836     }
837 }
838 
839 /**
840  * Closes all documents whose tabs are to the right of the index.
841  */
closeDocumentsToRight(int index)842 void DocumentManager::closeDocumentsToRight(int index)
843 {
844     if (index == -1)
845         return;
846 
847     mMultiDocumentClose = true;
848 
849     for (int i = mTabBar->count() - 1; i > index; --i) {
850         documentCloseRequested(i);
851 
852         if (!mMultiDocumentClose)
853             return;
854     }
855 }
856 
857 /**
858  * Closes the document at the given \a index. Will not ask the user whether
859  * to save any changes!
860  *
861  * The file is added to the list of recent files.
862  */
closeDocumentAt(int index)863 void DocumentManager::closeDocumentAt(int index)
864 {
865     auto document = mDocuments.at(index);       // keeps alive and may delete
866 
867     emit documentAboutToClose(document.data());
868 
869     mDocuments.removeAt(index);
870     mTabBar->removeTab(index);
871 
872     if (Editor *editor = mEditorForType.value(document->type()))
873         editor->removeDocument(document.data());
874 
875     if (!document->fileName().isEmpty()) {
876         mFileSystemWatcher->removePath(document->fileName());
877         document->setChangedOnDisk(false);
878     }
879 
880     if (auto mapDocument = qobject_cast<MapDocument*>(document.data())) {
881         for (const SharedTileset &tileset : mapDocument->map()->tilesets())
882             removeFromTilesetDocument(tileset, mapDocument);
883     } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(document.data())) {
884         if (tilesetDocument->mapDocuments().isEmpty()) {
885             mTilesetDocumentsModel->remove(tilesetDocument);
886             emit tilesetDocumentRemoved(tilesetDocument);
887         } else {
888             tilesetDocument->disconnect(this);
889         }
890     }
891 
892     if (!document->fileName().isEmpty())
893         Preferences::instance()->addRecentFile(document->fileName());
894 }
895 
896 /**
897  * Reloads the current document. Will not ask the user whether to save any
898  * changes!
899  *
900  * \sa reloadDocumentAt()
901  */
reloadCurrentDocument()902 bool DocumentManager::reloadCurrentDocument()
903 {
904     const int index = mTabBar->currentIndex();
905     if (index == -1)
906         return false;
907 
908     return reloadDocumentAt(index);
909 }
910 
911 /**
912  * Reloads the document at the given \a index. It will lose any undo
913  * history and current selections. Will not ask the user whether to save
914  * any changes!
915  *
916  * Returns whether the document loaded successfully.
917  */
reloadDocumentAt(int index)918 bool DocumentManager::reloadDocumentAt(int index)
919 {
920     const auto oldDocument = mDocuments.at(index);
921     QString error;
922 
923     if (auto mapDocument = oldDocument.objectCast<MapDocument>()) {
924         auto readerFormat = mapDocument->readerFormat();
925         if (!readerFormat)
926             return false;
927 
928         // TODO: Consider fixing the reload to avoid recreating the MapDocument
929         auto newDocument = MapDocument::load(oldDocument->fileName(),
930                                              readerFormat,
931                                              &error);
932         if (!newDocument) {
933             emit reloadError(tr("%1:\n\n%2").arg(oldDocument->fileName(), error));
934             return false;
935         }
936 
937         // Save the document state, to ensure the new document will match it
938         static_cast<MapEditor*>(editor(Document::MapDocumentType))->saveDocumentState(mapDocument.data());
939 
940         // Replace old tab
941         insertDocument(index, newDocument); // also selects the new document
942         closeDocumentAt(index + 1);
943 
944         checkTilesetColumns(newDocument.data());
945 
946     } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(oldDocument)) {
947         if (tilesetDocument->isEmbedded()) {
948             // For embedded tilesets, we need to reload the map
949             index = findDocument(tilesetDocument->mapDocuments().first());
950             if (!reloadDocumentAt(index))
951                 return false;
952         } else if (!tilesetDocument->reload(&error)) {
953             emit reloadError(tr("%1:\n\n%2").arg(oldDocument->fileName(), error));
954             return false;
955         }
956 
957         tilesetDocument->setChangedOnDisk(false);
958     }
959 
960     if (!isDocumentChangedOnDisk(currentDocument()))
961         mFileChangedWarning->setVisible(false);
962 
963     return true;
964 }
965 
currentIndexChanged()966 void DocumentManager::currentIndexChanged()
967 {
968     auto document = currentDocument();
969     Editor *editor = nullptr;
970     bool changed = false;
971 
972     if (document) {
973         editor = mEditorForType.value(document->type());
974         changed = isDocumentChangedOnDisk(document);
975     }
976 
977     QWidget *editorWidget = mNoEditorWidget;
978 
979     if (editor) {
980         editor->setCurrentDocument(document);
981         editorWidget = editor->editorWidget();
982     }
983 
984     if (mEditorStack->currentWidget() != editorWidget) {
985         mEditorStack->setCurrentWidget(editorWidget);
986         emit currentEditorChanged(editor);
987     }
988 
989     mFileChangedWarning->setVisible(changed);
990 
991     mBrokenLinksModel->setDocument(document);
992 
993     emit currentDocumentChanged(document);
994 }
995 
fileNameChanged(const QString & fileName,const QString & oldFileName)996 void DocumentManager::fileNameChanged(const QString &fileName,
997                                       const QString &oldFileName)
998 {
999     if (!fileName.isEmpty())
1000         mFileSystemWatcher->addPath(fileName);
1001     if (!oldFileName.isEmpty())
1002         mFileSystemWatcher->removePath(oldFileName);
1003 
1004     // Update the tabs for all opened embedded tilesets
1005     Document *document = static_cast<Document*>(sender());
1006     if (MapDocument *mapDocument = qobject_cast<MapDocument*>(document)) {
1007         for (const SharedTileset &tileset : mapDocument->map()->tilesets()) {
1008             if (auto tilesetDocument = findTilesetDocument(tileset))
1009                 updateDocumentTab(tilesetDocument);
1010         }
1011     }
1012 
1013     updateDocumentTab(document);
1014 }
1015 
updateDocumentTab(Document * document)1016 void DocumentManager::updateDocumentTab(Document *document)
1017 {
1018     const int index = findDocument(document);
1019     if (index == -1)
1020         return;
1021 
1022     QString tabText = document->displayName();
1023     if (document->isModified())
1024         tabText.prepend(QLatin1Char('*'));
1025 
1026     mTabBar->setTabText(index, tabText);
1027     mTabBar->setTabToolTip(index, document->fileName());
1028 }
1029 
onDocumentSaved()1030 void DocumentManager::onDocumentSaved()
1031 {
1032     Document *document = static_cast<Document*>(sender());
1033 
1034     if (document->changedOnDisk()) {
1035         document->setChangedOnDisk(false);
1036         if (!isDocumentModified(currentDocument()))
1037             mFileChangedWarning->setVisible(false);
1038     }
1039 }
1040 
documentTabMoved(int from,int to)1041 void DocumentManager::documentTabMoved(int from, int to)
1042 {
1043     mDocuments.move(from, to);
1044 }
1045 
tabContextMenuRequested(const QPoint & pos)1046 void DocumentManager::tabContextMenuRequested(const QPoint &pos)
1047 {
1048     int index = mTabBar->tabAt(pos);
1049     if (index == -1)
1050         return;
1051 
1052     QMenu menu(mTabBar->window());
1053 
1054     const Document *fileDocument = mDocuments.at(index).data();
1055     if (fileDocument->type() == Document::TilesetDocumentType) {
1056         auto tilesetDocument = static_cast<const TilesetDocument*>(fileDocument);
1057         if (tilesetDocument->isEmbedded())
1058             fileDocument = tilesetDocument->mapDocuments().first();
1059     }
1060 
1061     Utils::addFileManagerActions(menu, fileDocument->fileName());
1062 
1063     menu.addSeparator();
1064 
1065     QAction *closeTab = menu.addAction(tr("Close"), [this, index] {
1066         documentCloseRequested(index);
1067     });
1068     closeTab->setIcon(QIcon(QStringLiteral(":/images/16/window-close.png")));
1069     Utils::setThemeIcon(closeTab, "window-close");
1070 
1071     menu.addAction(tr("Close Other Tabs"), [this, index] {
1072         closeOtherDocuments(index);
1073     });
1074 
1075     menu.addAction(tr("Close Tabs to the Right"), [this, index] {
1076         closeDocumentsToRight(index);
1077     });
1078 
1079     menu.exec(mTabBar->mapToGlobal(pos));
1080 }
1081 
tilesetAdded(int index,Tileset * tileset)1082 void DocumentManager::tilesetAdded(int index, Tileset *tileset)
1083 {
1084     Q_UNUSED(index)
1085     MapDocument *mapDocument = static_cast<MapDocument*>(QObject::sender());
1086     addToTilesetDocument(tileset->sharedPointer(), mapDocument);
1087 }
1088 
tilesetRemoved(Tileset * tileset)1089 void DocumentManager::tilesetRemoved(Tileset *tileset)
1090 {
1091     MapDocument *mapDocument = static_cast<MapDocument*>(QObject::sender());
1092     removeFromTilesetDocument(tileset->sharedPointer(), mapDocument);
1093 }
1094 
tilesetNameChanged(Tileset * tileset)1095 void DocumentManager::tilesetNameChanged(Tileset *tileset)
1096 {
1097     auto *tilesetDocument = findTilesetDocument(tileset->sharedPointer());
1098     if (tilesetDocument->isEmbedded())
1099         updateDocumentTab(tilesetDocument);
1100 }
1101 
filesChanged(const QStringList & fileNames)1102 void DocumentManager::filesChanged(const QStringList &fileNames)
1103 {
1104     for (const QString &fileName : fileNames)
1105         fileChanged(fileName);
1106 }
1107 
fileChanged(const QString & fileName)1108 void DocumentManager::fileChanged(const QString &fileName)
1109 {
1110     const int index = findDocument(fileName);
1111 
1112     // Most likely the file was removed
1113     if (index == -1)
1114         return;
1115 
1116     const auto &document = mDocuments.at(index);
1117 
1118     // Ignore change event when it seems to be our own save
1119     if (QFileInfo(fileName).lastModified() == document->lastSaved())
1120         return;
1121 
1122     // Automatically reload when there are no unsaved changes
1123     if (!isDocumentModified(document.data())) {
1124         reloadDocumentAt(index);
1125         return;
1126     }
1127 
1128     document->setChangedOnDisk(true);
1129 
1130     if (isDocumentChangedOnDisk(currentDocument()))
1131         mFileChangedWarning->setVisible(true);
1132 }
1133 
hideChangedWarning()1134 void DocumentManager::hideChangedWarning()
1135 {
1136     Document *document = currentDocument();
1137     if (auto tilesetDocument = qobject_cast<TilesetDocument*>(document)) {
1138         if (tilesetDocument->isEmbedded())
1139             document = tilesetDocument->mapDocuments().first();
1140     }
1141 
1142     document->setChangedOnDisk(false);
1143     mFileChangedWarning->setVisible(false);
1144 }
1145 
findTilesetDocument(const SharedTileset & tileset) const1146 TilesetDocument* DocumentManager::findTilesetDocument(const SharedTileset &tileset) const
1147 {
1148     return TilesetDocument::findDocumentForTileset(tileset);
1149 }
1150 
findTilesetDocument(const QString & fileName) const1151 TilesetDocument* DocumentManager::findTilesetDocument(const QString &fileName) const
1152 {
1153     const QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
1154     if (canonicalFilePath.isEmpty()) // file doesn't exist
1155         return nullptr;
1156 
1157     for (const auto &tilesetDocument : mTilesetDocumentsModel->tilesetDocuments()) {
1158         QString name = tilesetDocument->fileName();
1159         if (!name.isEmpty() && QFileInfo(name).canonicalFilePath() == canonicalFilePath)
1160             return tilesetDocument.data();
1161     }
1162 
1163     return nullptr;
1164 }
1165 
1166 /**
1167  * Opens the document for the given \a tileset.
1168  */
openTileset(const SharedTileset & tileset)1169 void DocumentManager::openTileset(const SharedTileset &tileset)
1170 {
1171     TilesetDocumentPtr tilesetDocument;
1172     if (auto existingTilesetDocument = findTilesetDocument(tileset))
1173         tilesetDocument = existingTilesetDocument->sharedFromThis();
1174     else
1175         tilesetDocument = TilesetDocumentPtr::create(tileset);
1176 
1177     if (!switchToDocument(tilesetDocument.data()))
1178         addDocument(tilesetDocument);
1179 }
1180 
addToTilesetDocument(const SharedTileset & tileset,MapDocument * mapDocument)1181 void DocumentManager::addToTilesetDocument(const SharedTileset &tileset, MapDocument *mapDocument)
1182 {
1183     if (auto existingTilesetDocument = findTilesetDocument(tileset)) {
1184         existingTilesetDocument->addMapDocument(mapDocument);
1185     } else {
1186         // Create TilesetDocument instance when it doesn't exist yet
1187         auto tilesetDocument = TilesetDocumentPtr::create(tileset);
1188         tilesetDocument->addMapDocument(mapDocument);
1189 
1190         mTilesetDocumentsModel->append(tilesetDocument.data());
1191         emit tilesetDocumentAdded(tilesetDocument.data());
1192     }
1193 }
1194 
removeFromTilesetDocument(const SharedTileset & tileset,MapDocument * mapDocument)1195 void DocumentManager::removeFromTilesetDocument(const SharedTileset &tileset, MapDocument *mapDocument)
1196 {
1197     auto tilesetDocument = findTilesetDocument(tileset);
1198     auto tilesetDocumentPtr = tilesetDocument->sharedFromThis();    // keeps alive and may delete
1199 
1200     tilesetDocument->removeMapDocument(mapDocument);
1201 
1202     bool unused = tilesetDocument->mapDocuments().isEmpty();
1203     bool external = tilesetDocument->tileset()->isExternal();
1204     int index = findDocument(tilesetDocument);
1205 
1206     // Remove the TilesetDocument when its tileset is no longer reachable
1207     if (unused && !(index >= 0 && external)) {
1208         if (index != -1) {
1209             closeDocumentAt(index);
1210         } else {
1211             mTilesetDocumentsModel->remove(tilesetDocument);
1212             emit tilesetDocumentRemoved(tilesetDocument);
1213         }
1214     }
1215 }
1216 
updateSession() const1217 void DocumentManager::updateSession() const
1218 {
1219     QStringList fileList;
1220     for (const auto &document : mDocuments) {
1221         if (!document->fileName().isEmpty())
1222             fileList.append(document->fileName());
1223     }
1224 
1225     auto doc = currentDocument();
1226 
1227     auto &session = Session::current();
1228     session.setOpenFiles(fileList);
1229     session.setActiveFile(doc ? doc->fileName() : QString());
1230 }
1231 
openMapFile(const QString & path)1232 MapDocument *DocumentManager::openMapFile(const QString &path)
1233 {
1234     openFile(path);
1235     const int i = findDocument(path);
1236     return i == -1 ? nullptr : qobject_cast<MapDocument*>(mDocuments.at(i).data());
1237 }
1238 
openTilesetFile(const QString & path)1239 TilesetDocument *DocumentManager::openTilesetFile(const QString &path)
1240 {
1241     openFile(path);
1242     const int i = findDocument(path);
1243     return i == -1 ? nullptr : qobject_cast<TilesetDocument*>(mDocuments.at(i).data());
1244 }
1245 
ensureWorldDocument(const QString & fileName)1246 WorldDocument *DocumentManager::ensureWorldDocument(const QString &fileName)
1247 {
1248     if (!mWorldDocuments.contains(fileName)) {
1249         WorldDocument* worldDocument = new WorldDocument(fileName);
1250         mWorldDocuments.insert(fileName, worldDocument);
1251         mUndoGroup->addStack(worldDocument->undoStack());
1252     }
1253     return mWorldDocuments[fileName];
1254 }
1255 
isAnyWorldModified() const1256 bool DocumentManager::isAnyWorldModified() const
1257 {
1258     for (const World *world : WorldManager::instance().worlds())
1259         if (isWorldModified(world->fileName))
1260             return true;
1261 
1262     return false;
1263 }
1264 
isWorldModified(const QString & fileName) const1265 bool DocumentManager::isWorldModified(const QString &fileName) const
1266 {
1267     if (const auto worldDocument = mWorldDocuments.value(fileName))
1268         return !worldDocument->undoStack()->isClean();
1269     return false;
1270 }
1271 
1272 /**
1273  * Returns a logical start location for a file dialog to open a file, based on
1274  * the currently selected file, a recent file, the project path or finally, the
1275  * home location.
1276  */
fileDialogStartLocation() const1277 QString DocumentManager::fileDialogStartLocation() const
1278 {
1279     if (auto doc = currentDocument()) {
1280         QString path = QFileInfo(doc->fileName()).path();
1281         if (!path.isEmpty())
1282             return path;
1283     }
1284 
1285     const auto &session = Session::current();
1286     if (!session.recentFiles.isEmpty())
1287         return QFileInfo(session.recentFiles.first()).path();
1288 
1289     const auto &project = ProjectManager::instance()->project();
1290     if (!project.fileName().isEmpty())
1291         return QFileInfo(project.fileName()).path();
1292 
1293     return Preferences::homeLocation();
1294 }
1295 
onWorldUnloaded(const QString & worldFile)1296 void DocumentManager::onWorldUnloaded(const QString &worldFile)
1297 {
1298     delete mWorldDocuments.take(worldFile);
1299 }
1300 
mayNeedColumnCountAdjustment(const Tileset & tileset)1301 static bool mayNeedColumnCountAdjustment(const Tileset &tileset)
1302 {
1303     if (tileset.isCollection())
1304         return false;
1305     if (tileset.imageStatus() != LoadingReady)
1306         return false;
1307     if (tileset.columnCount() == tileset.expectedColumnCount())
1308         return false;
1309     if (tileset.columnCount() == 0 || tileset.expectedColumnCount() == 0)
1310         return false;
1311     if (tileset.expectedRowCount() < 2 || tileset.rowCount() < 2)
1312         return false;
1313 
1314     return true;
1315 }
1316 
tilesetImagesChanged(Tileset * tileset)1317 void DocumentManager::tilesetImagesChanged(Tileset *tileset)
1318 {
1319     if (!mayNeedColumnCountAdjustment(*tileset))
1320         return;
1321 
1322     SharedTileset sharedTileset = tileset->sharedPointer();
1323     QList<Document*> affectedDocuments;
1324 
1325     for (const auto &document : qAsConst(mDocuments)) {
1326         if (auto mapDocument = qobject_cast<MapDocument*>(document.data())) {
1327             if (mapDocument->map()->tilesets().contains(sharedTileset))
1328                 affectedDocuments.append(document.data());
1329         }
1330     }
1331 
1332     if (TilesetDocument *tilesetDocument = findTilesetDocument(sharedTileset))
1333         affectedDocuments.append(tilesetDocument);
1334 
1335     if (!affectedDocuments.isEmpty() && askForAdjustment(*tileset)) {
1336         for (Document *document : qAsConst(affectedDocuments)) {
1337             if (auto mapDocument = qobject_cast<MapDocument*>(document)) {
1338                 auto command = new AdjustTileIndexes(mapDocument, *tileset);
1339                 document->undoStack()->push(command);
1340             } else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(document)) {
1341                 auto command = new AdjustTileMetaData(tilesetDocument);
1342                 document->undoStack()->push(command);
1343             }
1344         }
1345     }
1346 
1347     tileset->syncExpectedColumnsAndRows();
1348 }
1349 
1350 /**
1351  * Checks whether the number of columns in tileset image based tilesets matches
1352  * with the expected amount. Offers to adjust tile indexes if not.
1353  */
checkTilesetColumns(MapDocument * mapDocument)1354 void DocumentManager::checkTilesetColumns(MapDocument *mapDocument)
1355 {
1356     for (const SharedTileset &tileset : mapDocument->map()->tilesets()) {
1357         TilesetDocument *tilesetDocument = findTilesetDocument(tileset);
1358         Q_ASSERT(tilesetDocument);
1359 
1360         if (checkTilesetColumns(tilesetDocument)) {
1361             auto command = new AdjustTileIndexes(mapDocument, *tileset);
1362             mapDocument->undoStack()->push(command);
1363         }
1364 
1365         tileset->syncExpectedColumnsAndRows();
1366     }
1367 }
1368 
checkTilesetColumns(TilesetDocument * tilesetDocument)1369 bool DocumentManager::checkTilesetColumns(TilesetDocument *tilesetDocument)
1370 {
1371     if (!mayNeedColumnCountAdjustment(*tilesetDocument->tileset()))
1372         return false;
1373 
1374     if (askForAdjustment(*tilesetDocument->tileset())) {
1375         auto command = new AdjustTileMetaData(tilesetDocument);
1376         tilesetDocument->undoStack()->push(command);
1377         return true;
1378     }
1379 
1380     return false;
1381 }
1382 
askForAdjustment(const Tileset & tileset)1383 bool DocumentManager::askForAdjustment(const Tileset &tileset)
1384 {
1385     int r = QMessageBox::question(mWidget->window(),
1386                                   tr("Tileset Columns Changed"),
1387                                   tr("The number of tile columns in the tileset '%1' appears to have changed from %2 to %3. "
1388                                      "Do you want to adjust tile references?")
1389                                   .arg(tileset.name())
1390                                   .arg(tileset.expectedColumnCount())
1391                                   .arg(tileset.columnCount()),
1392                                   QMessageBox::Yes | QMessageBox::No,
1393                                   QMessageBox::Yes);
1394 
1395     return r == QMessageBox::Yes;
1396 }
1397 
1398 /**
1399  * Unsets a flag to stop closeOtherDocuments() and closeDocumentsToRight()
1400  * when Cancel is pressed
1401  */
abortMultiDocumentClose()1402 void DocumentManager::abortMultiDocumentClose()
1403 {
1404     mMultiDocumentClose = false;
1405 }
1406 
1407 #include "moc_documentmanager.cpp"
1408