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