1 /*
2  * tilesetdock.cpp
3  * Copyright 2008-2010, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
4  * Copyright 2009, Edward Hutchins <eah1@yahoo.com>
5  * Copyright 2012, Stefan Beller <stefanbeller@googlemail.com>
6  *
7  * This file is part of Tiled.
8  *
9  * This program is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU General Public License as published by the Free
11  * Software Foundation; either version 2 of the License, or (at your option)
12  * any later version.
13  *
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
17  * more details.
18  *
19  * You should have received a copy of the GNU General Public License along with
20  * this program. If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 #include "tilesetdock.h"
24 
25 #include "actionmanager.h"
26 #include "addremovemapobject.h"
27 #include "addremovetileset.h"
28 #include "containerhelpers.h"
29 #include "documentmanager.h"
30 #include "editablemanager.h"
31 #include "editabletile.h"
32 #include "erasetiles.h"
33 #include "map.h"
34 #include "mapdocument.h"
35 #include "mapobject.h"
36 #include "objectgroup.h"
37 #include "preferences.h"
38 #include "replacetileset.h"
39 #include "scriptmanager.h"
40 #include "session.h"
41 #include "swaptiles.h"
42 #include "tabbar.h"
43 #include "tile.h"
44 #include "tilelayer.h"
45 #include "tilesetdocument.h"
46 #include "tilesetdocumentsmodel.h"
47 #include "tilesetformat.h"
48 #include "tilesetmanager.h"
49 #include "tilesetmodel.h"
50 #include "tilesetview.h"
51 #include "tilestamp.h"
52 #include "tmxmapformat.h"
53 #include "utils.h"
54 #include "zoomable.h"
55 
56 #include <QAction>
57 #include <QActionGroup>
58 #include <QComboBox>
59 #include <QDropEvent>
60 #include <QFileDialog>
61 #include <QHBoxLayout>
62 #include <QMenu>
63 #include <QMessageBox>
64 #include <QMimeData>
65 #include <QPushButton>
66 #include <QScopedValueRollback>
67 #include <QStackedWidget>
68 #include <QStylePainter>
69 #include <QToolBar>
70 #include <QToolButton>
71 #include <QUrl>
72 #include <QVBoxLayout>
73 
74 #include <functional>
75 
76 using namespace Tiled;
77 
78 namespace {
79 
80 class NoTilesetWidget : public QWidget
81 {
82     Q_OBJECT
83 
84 public:
NoTilesetWidget(QWidget * parent=nullptr)85     explicit NoTilesetWidget(QWidget *parent = nullptr)
86         : QWidget(parent)
87     {
88         QPushButton *newTilesetButton = new QPushButton(this);
89         newTilesetButton->setText(tr("New Tileset..."));
90 
91         QGridLayout *gridLayout = new QGridLayout(this);
92         gridLayout->addWidget(newTilesetButton, 0, 0, Qt::AlignCenter);
93 
94         connect(newTilesetButton, &QPushButton::clicked, [] {
95             ActionManager::action("NewTileset")->trigger();
96         });
97     }
98 };
99 
100 class TilesetMenuButton : public QToolButton
101 {
102 public:
TilesetMenuButton(QWidget * parent=nullptr)103     explicit TilesetMenuButton(QWidget *parent = nullptr)
104         : QToolButton(parent)
105     {
106         setArrowType(Qt::DownArrow);
107         setIconSize(Utils::smallIconSize());
108         setPopupMode(QToolButton::InstantPopup);
109         setAutoRaise(true);
110 
111         setSizePolicy(sizePolicy().horizontalPolicy(),
112                       QSizePolicy::Ignored);
113     }
114 
115 protected:
paintEvent(QPaintEvent *)116     void paintEvent(QPaintEvent *) override
117     {
118         QStylePainter p(this);
119         QStyleOptionToolButton opt;
120         initStyleOption(&opt);
121 
122         // Disable the menu arrow, since we already got a down arrow icon
123         opt.features &= ~QStyleOptionToolButton::HasMenu;
124 
125         p.drawComplexControl(QStyle::CC_ToolButton, opt);
126     }
127 };
128 
129 
removeTileReferences(MapDocument * mapDocument,std::function<bool (const Cell &)> condition)130 static void removeTileReferences(MapDocument *mapDocument,
131                                  std::function<bool(const Cell &)> condition)
132 {
133     QUndoStack *undoStack = mapDocument->undoStack();
134 
135     QList<MapObject*> objectsToRemove;
136 
137     LayerIterator it(mapDocument->map());
138     while (Layer *layer = it.next()) {
139         switch (layer->layerType()) {
140         case Layer::TileLayerType: {
141             auto tileLayer = static_cast<TileLayer*>(layer);
142             const QRegion refs = tileLayer->region(condition);
143             if (!refs.isEmpty())
144                 undoStack->push(new EraseTiles(mapDocument, tileLayer, refs));
145             break;
146         }
147         case Layer::ObjectGroupType: {
148             auto objectGroup = static_cast<ObjectGroup*>(layer);
149             for (MapObject *object : *objectGroup) {
150                 if (condition(object->cell()))
151                     objectsToRemove.append(object);
152             }
153             break;
154         }
155         case Layer::ImageLayerType:
156         case Layer::GroupLayerType:
157             break;
158         }
159     }
160 
161     if (!objectsToRemove.isEmpty())
162         undoStack->push(new RemoveMapObjects(mapDocument, objectsToRemove));
163 }
164 
165 } // anonymous namespace
166 
TilesetDock(QWidget * parent)167 TilesetDock::TilesetDock(QWidget *parent)
168     : QDockWidget(parent)
169     , mTilesetDocumentsFilterModel(new TilesetDocumentsFilterModel(this))
170     , mTabBar(new TabBar)
171     , mSuperViewStack(new QStackedWidget)
172     , mViewStack(new QStackedWidget)
173     , mToolBar(new QToolBar)
174     , mNewTileset(new QAction(this))
175     , mEmbedTileset(new QAction(this))
176     , mExportTileset(new QAction(this))
177     , mEditTileset(new QAction(this))
178     , mReplaceTileset(new QAction(this))
179     , mRemoveTileset(new QAction(this))
180     , mSelectNextTileset(new QAction(this))
181     , mSelectPreviousTileset(new QAction(this))
182     , mDynamicWrappingToggle(new QAction(this))
183     , mTilesetMenuButton(new TilesetMenuButton(this))
184     , mTilesetMenu(new QMenu(this))
185     , mTilesetActionGroup(new QActionGroup(this))
186 {
187     setObjectName(QLatin1String("TilesetDock"));
188 
189     mSelectNextTileset->setShortcut(Qt::Key_BracketRight);
190     mSelectPreviousTileset->setShortcut(Qt::Key_BracketLeft);
191 
192     ActionManager::registerAction(mSelectNextTileset, "SelectNextTileset");
193     ActionManager::registerAction(mSelectPreviousTileset, "SelectPreviousTileset");
194 
195     mTabBar->setUsesScrollButtons(true);
196     mTabBar->setExpanding(false);
197     mTabBar->setContextMenuPolicy(Qt::CustomContextMenu);
198 
199     connect(mTabBar, &QTabBar::currentChanged, this, &TilesetDock::updateActions);
200     connect(mTabBar, &QTabBar::tabMoved, this, &TilesetDock::onTabMoved);
201     connect(mTabBar, &QWidget::customContextMenuRequested,
202             this, &TilesetDock::tabContextMenuRequested);
203 
204     QWidget *w = new QWidget(this);
205 
206     QHBoxLayout *horizontal = new QHBoxLayout;
207     horizontal->setSpacing(0);
208     horizontal->addWidget(mTabBar);
209     horizontal->addWidget(mTilesetMenuButton);
210 
211     QVBoxLayout *vertical = new QVBoxLayout(w);
212     vertical->setSpacing(0);
213     vertical->setContentsMargins(0, 0, 0, 0);
214     vertical->addLayout(horizontal);
215     vertical->addWidget(mSuperViewStack);
216 
217     mSuperViewStack->insertWidget(0, new NoTilesetWidget(this));
218     mSuperViewStack->insertWidget(1, mViewStack);
219 
220     horizontal = new QHBoxLayout;
221     horizontal->setSpacing(0);
222     horizontal->addWidget(mToolBar, 1);
223     vertical->addLayout(horizontal);
224 
225     mDynamicWrappingToggle->setCheckable(true);
226     mDynamicWrappingToggle->setIcon(QIcon(QLatin1String("://images/scalable/wrap.svg")));
227 
228     mNewTileset->setIcon(QIcon(QLatin1String(":images/16/document-new.png")));
229     mEmbedTileset->setIcon(QIcon(QLatin1String(":images/16/document-import.png")));
230     mExportTileset->setIcon(QIcon(QLatin1String(":images/16/document-export.png")));
231     mEditTileset->setIcon(QIcon(QLatin1String(":images/16/document-properties.png")));
232     mReplaceTileset->setIcon(QIcon(QLatin1String(":images/scalable/replace.svg")));
233     mRemoveTileset->setIcon(QIcon(QLatin1String(":images/16/edit-delete.png")));
234 
235     Utils::setThemeIcon(mNewTileset, "document-new");
236     Utils::setThemeIcon(mEmbedTileset, "document-import");
237     Utils::setThemeIcon(mExportTileset, "document-export");
238     Utils::setThemeIcon(mEditTileset, "document-properties");
239     Utils::setThemeIcon(mRemoveTileset, "edit-delete");
240 
241     connect(mNewTileset, &QAction::triggered, this, &TilesetDock::newTileset);
242     connect(mEmbedTileset, &QAction::triggered, this, &TilesetDock::embedTileset);
243     connect(mExportTileset, &QAction::triggered, this, &TilesetDock::exportTileset);
244     connect(mEditTileset, &QAction::triggered, this, &TilesetDock::editTileset);
245     connect(mReplaceTileset, &QAction::triggered, this, &TilesetDock::replaceTileset);
246     connect(mRemoveTileset, &QAction::triggered, this, &TilesetDock::removeTileset);
247     connect(mSelectNextTileset, &QAction::triggered, this, [this] { mTabBar->setCurrentIndex(mTabBar->currentIndex() + 1); });
248     connect(mSelectPreviousTileset, &QAction::triggered, this, [this] { mTabBar->setCurrentIndex(mTabBar->currentIndex() - 1); });
249     connect(mDynamicWrappingToggle, &QAction::toggled, this, [this] (bool checked) {
250         if (TilesetView *view = currentTilesetView()) {
251             view->setDynamicWrapping(checked);
252 
253             const QString fileName = currentTilesetDocument()->externalOrEmbeddedFileName();
254             Session::current().setFileStateValue(fileName, QLatin1String("dynamicWrapping"), checked);
255         }
256     });
257 
258     auto stretch = new QWidget;
259     stretch->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
260 
261     mToolBar->setIconSize(Utils::smallIconSize());
262     mToolBar->addAction(mNewTileset);
263     mToolBar->addAction(mEmbedTileset);
264     mToolBar->addAction(mExportTileset);
265     mToolBar->addAction(mEditTileset);
266     mToolBar->addAction(mReplaceTileset);
267     mToolBar->addAction(mRemoveTileset);
268     mToolBar->addWidget(stretch);
269     mToolBar->addAction(mDynamicWrappingToggle);
270 
271     mZoomComboBox = new QComboBox;
272     horizontal->addWidget(mZoomComboBox);
273 
274     connect(mViewStack, &QStackedWidget::currentChanged,
275             this, &TilesetDock::currentTilesetChanged);
276 
277     connect(TilesetManager::instance(), &TilesetManager::tilesetImagesChanged,
278             this, &TilesetDock::tilesetChanged);
279 
280     connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::rowsInserted,
281             this, &TilesetDock::onTilesetRowsInserted);
282     connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::rowsAboutToBeRemoved,
283             this, &TilesetDock::onTilesetRowsAboutToBeRemoved);
284     connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::rowsMoved,
285             this, &TilesetDock::onTilesetRowsMoved);
286     connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::layoutChanged,
287             this, &TilesetDock::onTilesetLayoutChanged);
288     connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::dataChanged,
289             this, &TilesetDock::onTilesetDataChanged);
290 
291     mTilesetMenuButton->setMenu(mTilesetMenu);
292     connect(mTilesetMenu, &QMenu::aboutToShow, this, &TilesetDock::refreshTilesetMenu);
293 
294     setWidget(w);
295     retranslateUi();
296     setAcceptDrops(true);
297 
298     updateActions();
299 }
300 
~TilesetDock()301 TilesetDock::~TilesetDock()
302 {
303 }
304 
setMapDocument(MapDocument * mapDocument)305 void TilesetDock::setMapDocument(MapDocument *mapDocument)
306 {
307     if (mMapDocument == mapDocument)
308         return;
309 
310     // Hide while we update the tab bar, to avoid repeated layouting
311     // But, this causes problems on OS X (issue #1055)
312 #ifndef Q_OS_OSX
313     widget()->hide();
314 #endif
315 
316     setCurrentTiles(nullptr);
317     setCurrentTile(nullptr);
318 
319     // Clear all connections to the previous document
320     if (mMapDocument)
321         mMapDocument->disconnect(this);
322 
323     mMapDocument = mapDocument;
324 
325     mTilesetDocumentsFilterModel->setMapDocument(mapDocument);
326 
327     if (mMapDocument) {
328         if (Object *object = mMapDocument->currentObject())
329             if (object->typeId() == Object::TileType)
330                 setCurrentTile(static_cast<Tile*>(object));
331 
332         connect(mMapDocument, &MapDocument::tilesetAdded,
333                 this, &TilesetDock::updateActions);
334         connect(mMapDocument, &MapDocument::tilesetRemoved,
335                 this, &TilesetDock::updateActions);
336         connect(mMapDocument, &MapDocument::tilesetReplaced,
337                 this, &TilesetDock::updateActions);
338     }
339 
340     updateActions();
341 
342 #ifndef Q_OS_OSX
343     widget()->show();
344 #endif
345 }
346 
347 /**
348  * Synchronizes the selection with the given stamp. Ignored when the stamp is
349  * changing because of a selection change in the TilesetDock.
350  */
selectTilesInStamp(const TileStamp & stamp)351 void TilesetDock::selectTilesInStamp(const TileStamp &stamp)
352 {
353     if (mEmittingStampCaptured)
354         return;
355 
356     QSet<Tile*> tiles;
357 
358     for (const TileStampVariation &variation : stamp.variations())
359         for (auto layer : variation.map->tileLayers())
360             for (const Cell &cell : *static_cast<TileLayer*>(layer))
361                 if (Tile *tile = cell.tile())
362                     tiles.insert(tile);
363 
364     selectTiles(tiles.values());
365 }
366 
selectTiles(const QList<Tile * > & tiles)367 void TilesetDock::selectTiles(const QList<Tile *> &tiles)
368 {
369     QHash<QItemSelectionModel*, QItemSelection> selections;
370 
371     for (Tile *tile : tiles) {
372         Tileset *tileset = tile->tileset();
373         int tilesetIndex = mTilesets.indexOf(tileset->sharedPointer());
374         if (tilesetIndex != -1) {
375             TilesetView *view = tilesetViewAt(tilesetIndex);
376             if (!view->model()) // Lazily set up the model
377                 setupTilesetModel(view, mTilesetDocuments.at(tilesetIndex));
378 
379             const TilesetModel *model = view->tilesetModel();
380             const QModelIndex modelIndex = model->tileIndex(tile);
381             QItemSelectionModel *selectionModel = view->selectionModel();
382             selections[selectionModel].select(modelIndex, modelIndex);
383         }
384     }
385 
386     if (!selections.isEmpty()) {
387         QScopedValueRollback<bool> synchronizingSelection(mSynchronizingSelection, true);
388 
389         // Mark tiles as selected
390         for (auto i = selections.constBegin(); i != selections.constEnd(); ++i) {
391             QItemSelectionModel *selectionModel = i.key();
392             const QItemSelection &selection = i.value();
393             selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
394         }
395 
396         // Update the current tile (useful for animation and collision editors)
397         auto first = selections.begin();
398         QItemSelectionModel *selectionModel = first.key();
399         const QItemSelection &selection = first.value();
400         const QModelIndex currentIndex = selection.first().topLeft();
401         if (selectionModel->currentIndex() != currentIndex)
402             selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate);
403         else
404             currentChanged(currentIndex);
405 
406         // If all of the selected tiles are in the same tileset, switch the
407         // current tab to that tileset.
408         if (selections.size() == 1) {
409             auto tileset = tiles.first()->tileset()->sharedPointer();
410             const int tilesetTabIndex = mTilesets.indexOf(tileset);
411             mTabBar->setCurrentIndex(tilesetTabIndex);
412         }
413     }
414 }
415 
changeEvent(QEvent * e)416 void TilesetDock::changeEvent(QEvent *e)
417 {
418     QDockWidget::changeEvent(e);
419     switch (e->type()) {
420     case QEvent::LanguageChange:
421         retranslateUi();
422         break;
423     default:
424         break;
425     }
426 }
427 
dragEnterEvent(QDragEnterEvent * e)428 void TilesetDock::dragEnterEvent(QDragEnterEvent *e)
429 {
430     const QList<QUrl> urls = e->mimeData()->urls();
431     if (!urls.isEmpty() && !urls.at(0).toLocalFile().isEmpty())
432         e->acceptProposedAction();
433 }
434 
dropEvent(QDropEvent * e)435 void TilesetDock::dropEvent(QDropEvent *e)
436 {
437     QStringList paths;
438     const auto urls = e->mimeData()->urls();
439     for (const QUrl &url : urls) {
440         const QString localFile = url.toLocalFile();
441         if (!localFile.isEmpty())
442             paths.append(localFile);
443     }
444     if (!paths.isEmpty()) {
445         emit localFilesDropped(paths);
446         e->acceptProposedAction();
447     }
448 }
449 
currentTilesetChanged()450 void TilesetDock::currentTilesetChanged()
451 {
452     TilesetView *view = currentTilesetView();
453     if (!view)
454         return;
455 
456     if (!mSynchronizingSelection)
457         updateCurrentTiles();
458 
459     view->zoomable()->setComboBox(mZoomComboBox);
460 
461     if (const QItemSelectionModel *s = view->selectionModel())
462         setCurrentTile(view->tilesetModel()->tileAt(s->currentIndex()));
463 
464     mDynamicWrappingToggle->setChecked(view->dynamicWrapping());
465 }
466 
selectionChanged()467 void TilesetDock::selectionChanged()
468 {
469     updateActions();
470 
471     if (!mSynchronizingSelection)
472         updateCurrentTiles();
473 }
474 
currentChanged(const QModelIndex & index)475 void TilesetDock::currentChanged(const QModelIndex &index)
476 {
477     if (!index.isValid())
478         return;
479 
480     const TilesetModel *model = static_cast<const TilesetModel*>(index.model());
481     setCurrentTile(model->tileAt(index));
482 }
483 
updateActions()484 void TilesetDock::updateActions()
485 {
486     bool external = false;
487     TilesetView *view = nullptr;
488     Tileset *tileset = nullptr;
489     const int index = mTabBar->currentIndex();
490 
491     if (index > -1) {
492         view = tilesetViewAt(index);
493         tileset = mTilesets.at(index).data();
494 
495         if (!view->model()) // Lazily set up the model
496             setupTilesetModel(view, mTilesetDocuments.at(index));
497 
498         mViewStack->setCurrentIndex(index);
499         external = tileset->isExternal();
500     }
501 
502     const auto map = mMapDocument ? mMapDocument->map() : nullptr;
503     const bool mapHasCurrentTileset = tileset && map && contains(map->tilesets(), tileset);
504 
505     mEmbedTileset->setEnabled(tileset && external);
506     mExportTileset->setEnabled(tileset && !external);
507     mEditTileset->setEnabled(tileset);
508     mReplaceTileset->setEnabled(mapHasCurrentTileset);
509     mRemoveTileset->setEnabled(mapHasCurrentTileset);
510     mSelectNextTileset->setEnabled(index != -1 && index < mTabBar->count() - 1);
511     mSelectPreviousTileset->setEnabled(index > 0);
512 }
513 
updateCurrentTiles()514 void TilesetDock::updateCurrentTiles()
515 {
516     TilesetView *view = currentTilesetView();
517     if (!view)
518         return;
519 
520     const QItemSelectionModel *s = view->selectionModel();
521     if (!s)
522         return;
523 
524     const QModelIndexList indexes = s->selection().indexes();
525     if (indexes.isEmpty())
526         return;
527 
528     const QModelIndex &first = indexes.first();
529     int minX = first.column();
530     int maxX = first.column();
531     int minY = first.row();
532     int maxY = first.row();
533 
534     for (const QModelIndex &index : indexes) {
535         if (minX > index.column()) minX = index.column();
536         if (maxX < index.column()) maxX = index.column();
537         if (minY > index.row()) minY = index.row();
538         if (maxY < index.row()) maxY = index.row();
539     }
540 
541     // Create a tile layer from the current selection
542     auto tileLayer = std::make_unique<TileLayer>(QString(), 0, 0,
543                                                  maxX - minX + 1,
544                                                  maxY - minY + 1);
545 
546     const TilesetModel *model = view->tilesetModel();
547     for (const QModelIndex &index : indexes) {
548         tileLayer->setCell(index.column() - minX,
549                            index.row() - minY,
550                            Cell(model->tileAt(index)));
551     }
552 
553     setCurrentTiles(std::move(tileLayer));
554 }
555 
indexPressed(const QModelIndex & index)556 void TilesetDock::indexPressed(const QModelIndex &index)
557 {
558     TilesetView *view = currentTilesetView();
559     if (Tile *tile = view->tilesetModel()->tileAt(index))
560         mMapDocument->setCurrentObject(tile, currentTilesetDocument());
561 }
562 
createTilesetView(int index,TilesetDocument * tilesetDocument)563 void TilesetDock::createTilesetView(int index, TilesetDocument *tilesetDocument)
564 {
565     auto tileset = tilesetDocument->tileset();
566 
567     mTilesets.insert(index, tileset);
568     mTilesetDocuments.insert(index, tilesetDocument);
569 
570     TilesetView *view = new TilesetView;
571 
572     // Hides the "New Tileset..." special view if it is shown.
573     mSuperViewStack->setCurrentIndex(1);
574 
575     // Restore state from last time
576     const QString fileName = tilesetDocument->externalOrEmbeddedFileName();
577     const QVariantMap fileState = Session::current().fileState(fileName);
578     if (fileState.isEmpty()) {
579         // Compatibility with Tiled 1.3
580         QString path = QLatin1String("TilesetDock/TilesetScale/") + tileset->name();
581         qreal scale = Preferences::instance()->value(path, 1).toReal();
582         view->zoomable()->setScale(scale);
583     } else {
584         bool ok;
585         const qreal scale = fileState.value(QLatin1String("scaleInDock")).toReal(&ok);
586         if (scale > 0 && ok)
587             view->zoomable()->setScale(scale);
588 
589         if (fileState.contains(QLatin1String("dynamicWrapping"))) {
590             const bool dynamicWrapping = fileState.value(QLatin1String("dynamicWrapping")).toBool();
591             view->setDynamicWrapping(dynamicWrapping);
592         }
593     }
594 
595     // Insert view before the tab to make sure it is there when the tab index
596     // changes (happens when first tab is inserted).
597     mViewStack->insertWidget(index, view);
598     mTabBar->insertTab(index, tileset->name());
599     mTabBar->setTabToolTip(index, tileset->fileName());
600 
601     connect(tilesetDocument, &TilesetDocument::fileNameChanged,
602             this, &TilesetDock::tilesetFileNameChanged);
603     connect(tilesetDocument, &TilesetDocument::tilesetChanged,
604             this, &TilesetDock::tilesetChanged);
605 
606     connect(view, &TilesetView::clicked,
607             this, &TilesetDock::updateCurrentTiles);
608     connect(view, &TilesetView::swapTilesRequested,
609             this, &TilesetDock::swapTiles);
610 }
611 
deleteTilesetView(int index)612 void TilesetDock::deleteTilesetView(int index)
613 {
614     TilesetDocument *tilesetDocument = mTilesetDocuments.at(index);
615     tilesetDocument->disconnect(this);
616 
617     Tileset *tileset = tilesetDocument->tileset().data();
618     TilesetView *view = tilesetViewAt(index);
619 
620     // Remember the scale
621     const QString fileName = tilesetDocument->externalOrEmbeddedFileName();
622     Session::current().setFileStateValue(fileName, QLatin1String("scaleInDock"), view->scale());
623 
624     // Some cleanup for potentially old preferences from Tiled 1.3
625     const QString path = QLatin1String("TilesetDock/TilesetScale/") + tileset->name();
626     Preferences::instance()->remove(path);
627 
628     mTilesets.remove(index);
629     mTilesetDocuments.removeAt(index);
630     delete view;                    // view needs to go before the tab
631     mTabBar->removeTab(index);
632 
633     // Make the "New Tileset..." special tab reappear if there is no tileset open
634     if (mTilesets.isEmpty())
635         mSuperViewStack->setCurrentIndex(0);
636 
637     // Make sure we don't reference this tileset anymore
638     if (mCurrentTiles && mCurrentTiles->referencesTileset(tileset)) {
639         auto cleaned = std::unique_ptr<TileLayer>(mCurrentTiles->clone());
640         cleaned->removeReferencesToTileset(tileset);
641         setCurrentTiles(std::move(cleaned));
642     }
643     if (mCurrentTile && mCurrentTile->tileset() == tileset)
644         setCurrentTile(nullptr);
645 }
646 
moveTilesetView(int from,int to)647 void TilesetDock::moveTilesetView(int from, int to)
648 {
649     mTabBar->moveTab(from, to);
650 }
651 
tilesetChanged(Tileset * tileset)652 void TilesetDock::tilesetChanged(Tileset *tileset)
653 {
654     // Update the affected tileset model, if it exists
655     const int index = indexOf(mTilesets, tileset);
656     if (index < 0)
657         return;
658 
659     TilesetView *view = tilesetViewAt(index);
660 
661     if (TilesetModel *model = view->tilesetModel()) {
662         view->updateBackgroundColor();
663         model->tilesetChanged();
664     }
665 }
666 
667 /**
668  * Offers to replace the currently selected tileset.
669  */
replaceTileset()670 void TilesetDock::replaceTileset()
671 {
672     const int currentIndex = mViewStack->currentIndex();
673     if (currentIndex == -1)
674         return;
675 
676     replaceTilesetAt(currentIndex);
677 }
678 
replaceTilesetAt(int index)679 void TilesetDock::replaceTilesetAt(int index)
680 {
681     if (!mMapDocument)
682         return;
683 
684     auto &sharedTileset = mTilesets.at(index);
685     int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(sharedTileset);
686     if (mapTilesetIndex == -1)
687         return;
688 
689     SessionOption<QString> lastUsedTilesetFilter { "tileset.lastUsedFilter" };
690     QString filter = tr("All Files (*)");
691     QString selectedFilter = lastUsedTilesetFilter;
692     if (selectedFilter.isEmpty())
693         selectedFilter = TsxTilesetFormat().nameFilter();
694 
695     FormatHelper<TilesetFormat> helper(FileFormat::Read, filter);
696 
697     Session &session = Session::current();
698     QString start = session.lastPath(Session::ExternalTileset);
699 
700     const auto fileName =
701             QFileDialog::getOpenFileName(this, tr("Replace Tileset"),
702                                          start,
703                                          helper.filter(),
704                                          &selectedFilter);
705 
706     if (fileName.isEmpty())
707         return;
708 
709     session.setLastPath(Session::ExternalTileset, QFileInfo(fileName).path());
710 
711     lastUsedTilesetFilter = selectedFilter;
712 
713     QString error;
714     SharedTileset tileset = TilesetManager::instance()->loadTileset(fileName, &error);
715     if (!tileset) {
716         QMessageBox::critical(window(), tr("Error Reading Tileset"), error);
717         return;
718     }
719 
720     // Don't try to replace a tileset with itself
721     if (tileset == sharedTileset)
722         return;
723 
724     QUndoCommand *command = new ReplaceTileset(mMapDocument,
725                                                mapTilesetIndex,
726                                                tileset);
727     mMapDocument->undoStack()->push(command);
728 }
729 
730 /**
731  * Removes the currently selected tileset.
732  */
removeTileset()733 void TilesetDock::removeTileset()
734 {
735     const int currentIndex = mViewStack->currentIndex();
736     if (currentIndex != -1)
737         removeTilesetAt(mViewStack->currentIndex());
738 }
739 
740 /**
741  * Removes the tileset at the given index. Prompting the user when the tileset
742  * is in use by the map.
743  */
removeTilesetAt(int index)744 void TilesetDock::removeTilesetAt(int index)
745 {
746     auto &sharedTileset = mTilesets.at(index);
747 
748     int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(sharedTileset);
749     if (mapTilesetIndex == -1)
750         return;
751 
752     Tileset *tileset = sharedTileset.data();
753     const bool inUse = mMapDocument->map()->isTilesetUsed(tileset);
754 
755     // If the tileset is in use, warn the user and confirm removal
756     if (inUse) {
757         QMessageBox warning(QMessageBox::Warning,
758                             tr("Remove Tileset"),
759                             tr("The tileset \"%1\" is still in use by the "
760                                "map!").arg(tileset->name()),
761                             QMessageBox::Yes | QMessageBox::No,
762                             this);
763         warning.setDefaultButton(QMessageBox::Yes);
764         warning.setInformativeText(tr("Remove this tileset and all references "
765                                       "to the tiles in this tileset?"));
766 
767         if (warning.exec() != QMessageBox::Yes)
768             return;
769     }
770 
771     QUndoCommand *remove = new RemoveTileset(mMapDocument, mapTilesetIndex);
772     QUndoStack *undoStack = mMapDocument->undoStack();
773 
774     if (inUse) {
775         // Remove references to tiles in this tileset from the current map
776         auto referencesTileset = [tileset] (const Cell &cell) {
777             return cell.tileset() == tileset;
778         };
779         undoStack->beginMacro(remove->text());
780         removeTileReferences(mMapDocument, referencesTileset);
781     }
782     undoStack->push(remove);
783     if (inUse)
784         undoStack->endMacro();
785 }
786 
newTileset()787 void TilesetDock::newTileset()
788 {
789     ActionManager::action("NewTileset")->trigger();
790 }
791 
setCurrentTiles(std::unique_ptr<TileLayer> tiles)792 void TilesetDock::setCurrentTiles(std::unique_ptr<TileLayer> tiles)
793 {
794     if (mCurrentTiles == tiles)
795         return;
796 
797     mCurrentTiles = std::move(tiles);
798 
799     if (mCurrentTiles && mMapDocument) {
800         // Create a tile stamp with these tiles
801         Map::Parameters mapParameters = mMapDocument->map()->parameters();
802         mapParameters.width = mCurrentTiles->width();
803         mapParameters.height = mCurrentTiles->height();
804         mapParameters.infinite = false;
805 
806         auto stamp = std::make_unique<Map>(mapParameters);
807         stamp->addLayer(mCurrentTiles->clone());
808         stamp->addTilesets(mCurrentTiles->usedTilesets());
809 
810         QScopedValueRollback<bool> emittingStampCaptured(mEmittingStampCaptured, true);
811         emit stampCaptured(TileStamp(std::move(stamp)));
812     }
813 }
814 
setCurrentTile(Tile * tile)815 void TilesetDock::setCurrentTile(Tile *tile)
816 {
817     if (mCurrentTile == tile)
818         return;
819 
820     mCurrentTile = tile;
821     emit currentTileChanged(tile);
822 
823     if (mMapDocument && tile) {
824         int tilesetIndex = indexOf(mTilesets, tile->tileset());
825         if (tilesetIndex != -1)
826             mMapDocument->setCurrentObject(tile, mTilesetDocuments.at(tilesetIndex));
827     }
828 }
829 
retranslateUi()830 void TilesetDock::retranslateUi()
831 {
832     setWindowTitle(tr("Tilesets"));
833     mNewTileset->setText(tr("New Tileset"));
834     mEmbedTileset->setText(tr("&Embed Tileset"));
835     mExportTileset->setText(tr("&Export Tileset As..."));
836     mEditTileset->setText(tr("Edit Tile&set"));
837     mReplaceTileset->setText(tr("Replace Tileset"));
838     mRemoveTileset->setText(tr("&Remove Tileset"));
839     mSelectNextTileset->setText(tr("Select Next Tileset"));
840     mSelectPreviousTileset->setText(tr("Select Previous Tileset"));
841     mDynamicWrappingToggle->setText(tr("Dynamically Wrap Tiles"));
842 }
843 
onTilesetRowsInserted(const QModelIndex & parent,int first,int last)844 void TilesetDock::onTilesetRowsInserted(const QModelIndex &parent, int first, int last)
845 {
846     for (int row = first; row <= last; ++row) {
847         const QModelIndex index = mTilesetDocumentsFilterModel->index(row, 0, parent);
848         const QVariant var = mTilesetDocumentsFilterModel->data(index, TilesetDocumentsModel::TilesetDocumentRole);
849         createTilesetView(row, var.value<TilesetDocument*>());
850     }
851 }
852 
onTilesetRowsAboutToBeRemoved(const QModelIndex & parent,int first,int last)853 void TilesetDock::onTilesetRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
854 {
855     Q_UNUSED(parent)
856 
857     for (int index = last; index >= first; --index)
858         deleteTilesetView(index);
859 }
860 
onTilesetRowsMoved(const QModelIndex & parent,int start,int end,const QModelIndex & destination,int row)861 void TilesetDock::onTilesetRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row)
862 {
863     Q_UNUSED(parent)
864     Q_UNUSED(destination)
865 
866     if (start == row)
867         return;
868 
869     while (start <= end) {
870         moveTilesetView(start, row);
871 
872         if (row < start) {
873             ++start;
874             ++row;
875         } else {
876             --end;
877         }
878     }
879 }
880 
onTilesetLayoutChanged(const QList<QPersistentModelIndex> & parents,QAbstractItemModel::LayoutChangeHint hint)881 void TilesetDock::onTilesetLayoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint)
882 {
883     Q_UNUSED(parents)
884     Q_UNUSED(hint)
885 
886     // Make sure the tileset tabs and views are still in the right order
887     for (int i = 0, rows = mTilesetDocumentsFilterModel->rowCount(); i < rows; ++i) {
888         const QModelIndex index = mTilesetDocumentsFilterModel->index(i, 0);
889         const QVariant var = mTilesetDocumentsFilterModel->data(index, TilesetDocumentsModel::TilesetDocumentRole);
890         TilesetDocument *tilesetDocument = var.value<TilesetDocument*>();
891         int currentIndex = mTilesetDocuments.indexOf(tilesetDocument);
892         if (currentIndex != i) {
893             Q_ASSERT(currentIndex > i);
894             moveTilesetView(currentIndex, i);
895         }
896     }
897 }
898 
onTilesetDataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight)899 void TilesetDock::onTilesetDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
900 {
901     // Update the titles of the affected tabs
902     for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
903         const SharedTileset &tileset = mTilesets.at(i);
904         if (mTabBar->tabText(i) != tileset->name())
905             mTabBar->setTabText(i, tileset->name());
906         mTabBar->setTabToolTip(i, tileset->fileName());
907     }
908 }
909 
onTabMoved(int from,int to)910 void TilesetDock::onTabMoved(int from, int to)
911 {
912     mTilesets.move(from, to);
913     mTilesetDocuments.move(from, to);
914 
915     // Move the related tileset view
916     const QSignalBlocker blocker(mViewStack);
917     QWidget *widget = mViewStack->widget(from);
918     mViewStack->removeWidget(widget);
919     mViewStack->insertWidget(to, widget);
920 }
921 
tabContextMenuRequested(const QPoint & pos)922 void TilesetDock::tabContextMenuRequested(const QPoint &pos)
923 {
924     int index = mTabBar->tabAt(pos);
925     if (index == -1)
926         return;
927 
928     QMenu menu;
929 
930     const QString fileName = mTilesetDocuments.at(index)->fileName();
931     Utils::addFileManagerActions(menu, fileName);
932 
933     menu.addSeparator();
934     menu.addAction(mEditTileset->icon(), mEditTileset->text(), this, [tileset = mTilesets.at(index)] {
935         DocumentManager::instance()->openTileset(tileset);
936     });
937 
938     menu.exec(mTabBar->mapToGlobal(pos));
939 }
940 
setCurrentTileset(const SharedTileset & tileset)941 void TilesetDock::setCurrentTileset(const SharedTileset &tileset)
942 {
943     const int index = mTilesets.indexOf(tileset);
944     if (index != -1)
945         mTabBar->setCurrentIndex(index);
946 }
947 
currentTileset() const948 SharedTileset TilesetDock::currentTileset() const
949 {
950     const int index = mViewStack->currentIndex();
951     if (index == -1)
952         return {};
953 
954     return mTilesets.at(index);
955 }
956 
currentTilesetDocument() const957 TilesetDocument *TilesetDock::currentTilesetDocument() const
958 {
959     const int index = mViewStack->currentIndex();
960     if (index == -1)
961         return nullptr;
962 
963     return mTilesetDocuments.at(index);
964 }
965 
setCurrentEditableTileset(EditableTileset * tileset)966 void TilesetDock::setCurrentEditableTileset(EditableTileset *tileset)
967 {
968     if (!tileset) {
969         ScriptManager::instance().throwNullArgError(0);
970         return;
971     }
972     setCurrentTileset(tileset->tileset()->sharedPointer());
973 }
974 
currentEditableTileset() const975 EditableTileset *TilesetDock::currentEditableTileset() const
976 {
977     const int index = mTabBar->currentIndex();
978     if (index == -1)
979         return nullptr;
980 
981     return mTilesetDocuments.at(index)->editable();
982 }
983 
setSelectedTiles(const QList<QObject * > & tiles)984 void TilesetDock::setSelectedTiles(const QList<QObject *> &tiles)
985 {
986     QList<Tile*> plainTiles;
987 
988     for (QObject *objectTile : tiles) {
989         auto editableTile = qobject_cast<EditableTile*>(objectTile);
990         if (!editableTile) {
991             ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Not a tile"));
992             return;
993         }
994         plainTiles.append(editableTile->tile());
995     }
996 
997     selectTiles(plainTiles);
998 }
999 
selectedTiles() const1000 QList<QObject *> TilesetDock::selectedTiles() const
1001 {
1002     QList<QObject *> result;
1003 
1004     TilesetView *view = currentTilesetView();
1005     if (!view)
1006         return result;
1007 
1008     const QItemSelectionModel *s = view->selectionModel();
1009     if (!s)
1010         return result;
1011 
1012     const QModelIndexList indexes = s->selection().indexes();
1013     if (indexes.isEmpty())
1014         return result;
1015 
1016     EditableTileset *editableTileset = currentEditableTileset();
1017 
1018     const TilesetModel *model = view->tilesetModel();
1019     auto &editableManager = EditableManager::instance();
1020     for (const QModelIndex &index : indexes)
1021         if (Tile *tile = model->tileAt(index))
1022             result.append(editableManager.editableTile(editableTileset, tile));
1023 
1024     return result;
1025 }
1026 
currentTilesetView() const1027 TilesetView *TilesetDock::currentTilesetView() const
1028 {
1029     return static_cast<TilesetView *>(mViewStack->currentWidget());
1030 }
1031 
tilesetViewAt(int index) const1032 TilesetView *TilesetDock::tilesetViewAt(int index) const
1033 {
1034     return static_cast<TilesetView *>(mViewStack->widget(index));
1035 }
1036 
setupTilesetModel(TilesetView * view,TilesetDocument * tilesetDocument)1037 void TilesetDock::setupTilesetModel(TilesetView *view, TilesetDocument *tilesetDocument)
1038 {
1039     view->setModel(new TilesetModel(tilesetDocument, view));
1040 
1041     QItemSelectionModel *s = view->selectionModel();
1042     connect(s, &QItemSelectionModel::selectionChanged,
1043             this, &TilesetDock::selectionChanged);
1044     connect(s, &QItemSelectionModel::currentChanged,
1045             this, &TilesetDock::currentChanged);
1046     connect(view, &TilesetView::pressed,
1047             this, &TilesetDock::indexPressed);
1048 }
1049 
editTileset()1050 void TilesetDock::editTileset()
1051 {
1052     auto tileset = currentTileset();
1053     if (!tileset)
1054         return;
1055 
1056     DocumentManager *documentManager = DocumentManager::instance();
1057     documentManager->openTileset(tileset);
1058 }
1059 
exportTileset()1060 void TilesetDock::exportTileset()
1061 {
1062     auto tileset = currentTileset();
1063     if (!tileset)
1064         return;
1065 
1066     if (tileset->isExternal())
1067         return;
1068 
1069     int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(tileset);
1070     if (mapTilesetIndex == -1)
1071         return;
1072 
1073     // To export a tileset we clone it, since the tileset could now be used by
1074     // other maps. This ensures undo can take the map back to using an embedded
1075     // tileset, without affecting those other maps.
1076     SharedTileset externalTileset = tileset->clone();
1077 
1078     FormatHelper<TilesetFormat> helper(FileFormat::ReadWrite);
1079 
1080     Session &session = Session::current();
1081 
1082     QString suggestedFileName = session.lastPath(Session::ExternalTileset);
1083     suggestedFileName += QLatin1Char('/');
1084     suggestedFileName += externalTileset->name();
1085 
1086     const QLatin1String extension(".tsx");
1087     if (!suggestedFileName.endsWith(extension))
1088         suggestedFileName.append(extension);
1089 
1090     // todo: remember last used filter
1091     QString selectedFilter = TsxTilesetFormat().nameFilter();
1092     const QString fileName =
1093             QFileDialog::getSaveFileName(this, tr("Export Tileset"),
1094                                          suggestedFileName,
1095                                          helper.filter(), &selectedFilter);
1096 
1097     if (fileName.isEmpty())
1098         return;
1099 
1100     session.setLastPath(Session::ExternalTileset,
1101                         QFileInfo(fileName).path());
1102 
1103     TilesetFormat *format = helper.formatByNameFilter(selectedFilter);
1104     if (!format)
1105         return;     // can't happen
1106 
1107     if (!format->write(*externalTileset, fileName)) {
1108         QString error = format->errorString();
1109         QMessageBox::critical(window(),
1110                               tr("Export Tileset"),
1111                               tr("Error saving tileset: %1").arg(error));
1112         return;
1113     }
1114 
1115     externalTileset->setFileName(fileName);
1116     externalTileset->setFormat(format->shortName());
1117 
1118     QUndoCommand *command = new ReplaceTileset(mMapDocument,
1119                                                mapTilesetIndex,
1120                                                externalTileset);
1121     mMapDocument->undoStack()->push(command);
1122 
1123     // Make sure the external tileset is selected
1124     int externalTilesetIndex = mTilesets.indexOf(externalTileset);
1125     if (externalTilesetIndex != -1)
1126         mTabBar->setCurrentIndex(externalTilesetIndex);
1127 }
1128 
embedTileset()1129 void TilesetDock::embedTileset()
1130 {
1131     auto tileset = currentTileset();
1132     if (!tileset)
1133         return;
1134 
1135     if (!tileset->isExternal())
1136         return;
1137 
1138     // To embed a tileset we clone it, since further changes to that tileset
1139     // should not affect the exteral tileset.
1140     SharedTileset embeddedTileset = tileset->clone();
1141 
1142     QUndoStack *undoStack = mMapDocument->undoStack();
1143     int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(tileset);
1144 
1145     // Tileset may not be part of the map yet
1146     if (mapTilesetIndex == -1)
1147         undoStack->push(new AddTileset(mMapDocument, embeddedTileset));
1148     else
1149         undoStack->push(new ReplaceTileset(mMapDocument, mapTilesetIndex, embeddedTileset));
1150 
1151     // Make sure the embedded tileset is selected
1152     int embeddedTilesetIndex = mTilesets.indexOf(embeddedTileset);
1153     if (embeddedTilesetIndex != -1)
1154         mTabBar->setCurrentIndex(embeddedTilesetIndex);
1155 }
1156 
tilesetFileNameChanged(const QString & fileName)1157 void TilesetDock::tilesetFileNameChanged(const QString &fileName)
1158 {
1159     TilesetDocument *tilesetDocument = static_cast<TilesetDocument*>(sender());
1160     Tileset *tileset = tilesetDocument->tileset().data();
1161 
1162     const int index = indexOf(mTilesets, tileset);
1163     Q_ASSERT(index != -1);
1164 
1165     mTabBar->setTabToolTip(index, fileName);
1166 
1167     updateActions();
1168 }
1169 
refreshTilesetMenu()1170 void TilesetDock::refreshTilesetMenu()
1171 {
1172     mTilesetMenu->clear();
1173 
1174     const int currentIndex = mTabBar->currentIndex();
1175 
1176     for (int i = 0; i < mTabBar->count(); ++i) {
1177         QAction *action = mTilesetMenu->addAction(mTabBar->tabText(i),
1178                                                   [=] { mTabBar->setCurrentIndex(i); });
1179 
1180         action->setCheckable(true);
1181         mTilesetActionGroup->addAction(action);
1182         if (i == currentIndex)
1183             action->setChecked(true);
1184     }
1185 
1186     mTilesetMenu->addSeparator();
1187     mTilesetMenu->addAction(ActionManager::action("AddExternalTileset"));
1188 }
1189 
swapTiles(Tile * tileA,Tile * tileB)1190 void TilesetDock::swapTiles(Tile *tileA, Tile *tileB)
1191 {
1192     if (!mMapDocument)
1193         return;
1194 
1195     QUndoStack *undoStack = mMapDocument->undoStack();
1196     undoStack->push(new SwapTiles(mMapDocument, tileA, tileB));
1197 }
1198 
1199 #include "tilesetdock.moc"
1200 #include "moc_tilesetdock.cpp"
1201